UNPKG

azurite

Version:

An open source Azure Storage API compatible server

241 lines 12.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const utils_1 = require("../../common/utils/utils"); const QueueStorageContext_1 = tslib_1.__importDefault(require("../context/QueueStorageContext")); const StorageErrorFactory_1 = tslib_1.__importDefault(require("../errors/StorageErrorFactory")); const constants_1 = require("../utils/constants"); class QueueSharedKeyAuthenticator { constructor(dataStore, logger) { this.dataStore = dataStore; this.logger = logger; } async validate(req, context) { const queueContext = new QueueStorageContext_1.default(context); const account = queueContext.account; this.logger.info(`QueueSharedKeyAuthenticator:validate() Start validation against account shared key authentication.`, queueContext.contextID); const authHeaderValue = req.getHeader(constants_1.HeaderConstants.AUTHORIZATION); if (authHeaderValue === undefined) { this.logger.info( // tslint:disable-next-line:max-line-length `QueueSharedKeyAuthenticator:validate() Request doesn't include valid authentication header. Skip shared key authentication.`, queueContext.contextID); return undefined; } // TODO: Make following async const accountProperties = this.dataStore.getAccount(account); if (accountProperties === undefined) { this.logger.error(`QueueSharedKeyAuthenticator:validate() Invalid storage account ${account}.`, queueContext.contextID); throw StorageErrorFactory_1.default.ResourceNotFound(context.contextID); } const authType = authHeaderValue.split(" ")[0]; const authValue = authHeaderValue.split(" ")[1]; const headersToSign = this.getHeadersToSign(authType, req); const stringToSign = headersToSign + this.getCanonicalizedResourceString(authType, req, account, queueContext.authenticationPath); this.logger.info(`QueueSharedKeyAuthenticator:validate() [STRING TO SIGN]:${JSON.stringify(stringToSign)}`, queueContext.contextID); const signature1 = (0, utils_1.computeHMACSHA256)(stringToSign, accountProperties.key1); const authValue1 = `${account}:${signature1}`; this.logger.info(`QueueSharedKeyAuthenticator:validate() Calculated authentication header based on key1: ${authValue1}`, queueContext.contextID); if (authValue === authValue1) { this.logger.info(`QueueSharedKeyAuthenticator:validate() Signature 1 matched.`, queueContext.contextID); return true; } if (accountProperties.key2) { const signature2 = (0, utils_1.computeHMACSHA256)(stringToSign, accountProperties.key2); const authValue2 = `${account}:${signature2}`; this.logger.info(`QueueSharedKeyAuthenticator:validate() Calculated authentication header based on key2: ${authValue2}`, queueContext.contextID); if (authValue === authValue2) { this.logger.info(`QueueSharedKeyAuthenticator:validate() Signature 2 matched.`, queueContext.contextID); return true; } } if (context.context.isSecondary && queueContext.authenticationPath?.indexOf(account) === 1) { // JS/.net Track2 SDK will generate stringToSign from IP style URI with "-secondary" in authenticationPath, so will also compare signature with this kind stringToSign const stringToSign_secondary = headersToSign + this.getCanonicalizedResourceString(authType, req, account, // The authenticationPath looks like "/devstoreaccount1/queue", add "-secondary" after account name to "/devstoreaccount1-secondary/queue" queueContext.authenticationPath?.replace(account, account + "-secondary")); this.logger.info(`QueueSharedKeyAuthenticator:validate() [STRING TO SIGN_secondary]:${JSON.stringify(stringToSign_secondary)}`, queueContext.contextID); const signature1_secondary = (0, utils_1.computeHMACSHA256)(stringToSign_secondary, accountProperties.key1); const authValue1_secondary = `${account}:${signature1_secondary}`; this.logger.info(`QueueSharedKeyAuthenticator:validate() Calculated authentication header based on key1: ${authValue1_secondary}`, queueContext.contextID); if (authValue === authValue1_secondary) { this.logger.info(`QueueSharedKeyAuthenticator:validate() Signature 1_secondary matched.`, queueContext.contextID); return true; } if (accountProperties.key2) { const signature2_secondary = (0, utils_1.computeHMACSHA256)(stringToSign_secondary, accountProperties.key2); const authValue2_secondary = `${account}:${signature2_secondary}`; this.logger.info(`QueueSharedKeyAuthenticator:validate() Calculated authentication header based on key2: ${authValue2_secondary}`, queueContext.contextID); if (authValue === authValue2_secondary) { this.logger.info(`QueueSharedKeyAuthenticator:validate() Signature 2_secondary matched.`, queueContext.contextID); return true; } } } // this.logger.info(`[URL]:${req.getUrl()}`); // this.logger.info(`[HEADERS]:${req.getHeaders().toString()}`); // this.logger.info(`[KEY]: ${request.headers.get(HeaderConstants.AUTHORIZATION)}`); this.logger.info(`QueueSharedKeyAuthenticator:validate() Validation failed.`, queueContext.contextID); return false; } /** * Retrieve header value according to shared key sign rules. * @see https://docs.microsoft.com/en-us/rest/api/storageservices/authenticate-with-shared-key * * @private * @param {WebResource} request * @param {string} headerName * @returns {string} * @memberof SharedKeyCredentialPolicy */ getHeaderValueToSign(request, headerName) { const value = request.getHeader(headerName); if (!value) { return ""; } // When using version 2015-02-21 or later, if Content-Length is zero, then // set the Content-Length part of the StringToSign to an empty string. // https://docs.microsoft.com/en-us/rest/api/storageservices/authenticate-with-shared-key if (headerName === constants_1.HeaderConstants.CONTENT_LENGTH && value === "0") { return ""; } return value; } /** * To construct the CanonicalizedHeaders portion of the signature string, follow these steps: * 1. Retrieve all headers for the resource that begin with x-ms-, including the x-ms-date header. * 2. Convert each HTTP header name to lowercase. * 3. Sort the headers lexicographically by header name, in ascending order. * Each header may appear only once in the string. * 4. Replace any linear whitespace in the header value with a single space. * 5. Trim any whitespace around the colon in the header. * 6. Finally, append a new-line character to each canonicalized header in the resulting list. * Construct the CanonicalizedHeaders string by concatenating all headers in this list into a single string. * * @private * @param {IRequest} request * @returns {string} * @memberof SharedKeyCredentialPolicy */ getCanonicalizedHeadersString(request) { const headers = []; const headersObject = request.getHeaders(); for (const name in headersObject) { if (headersObject.hasOwnProperty(name)) { const value = headersObject[name]; if (value === undefined) { headers.push({ name, value: "" }); } else if (typeof value === "string") { headers.push({ name, value }); } else { headers.push({ name, value: value.join(",") }); } } } const headersArray = headers.filter(value => { return value.name .toLowerCase() .startsWith(constants_1.HeaderConstants.PREFIX_FOR_STORAGE); }); headersArray.sort((a, b) => { return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); }); let canonicalizedHeadersStringToSign = ""; headersArray.forEach(header => { canonicalizedHeadersStringToSign += `${header.name .toLowerCase() .trimRight()}:${header.value.trimLeft()}\n`; }); return canonicalizedHeadersStringToSign; } // TODO: doc /** * Retrieves canonicalized resource string. * * @private * @param {IRequest} request * @returns {string} * @memberof SharedKeyCredentialPolicy */ getCanonicalizedResourceString(type, request, account, authenticationPath) { let path = request.getPath() || "/"; // For secondary account, we use account name (without "-secondary") for the path if (authenticationPath !== undefined) { path = authenticationPath; } let canonicalizedResourceString = ""; canonicalizedResourceString += `/${account}${path}`; const queries = (0, utils_1.getURLQueries)(request.getUrl()); const lowercaseQueries = {}; if (queries) { if (type === "SharedKey") { const queryKeys = []; for (const key in queries) { if (queries.hasOwnProperty(key)) { const lowercaseKey = key.toLowerCase(); lowercaseQueries[lowercaseKey] = queries[key]; queryKeys.push(lowercaseKey); } } queryKeys.sort(); for (const key of queryKeys) { canonicalizedResourceString += `\n${key}:${decodeURIComponent(lowercaseQueries[key].replace(/\+/g, '%20'))}`; } } else if (type === "SharedKeyLite") { for (const key in queries) { if (queries.hasOwnProperty(key) && key.toLowerCase() === "comp") { canonicalizedResourceString += `?comp=${decodeURIComponent(queries[key].replace(/\+/g, '%20'))}`; } } } } return canonicalizedResourceString; } // TODO: DOC /** * Get the StringToSign of headers for SharedKey or SharedKeyLite * * @private * @param {"SharedKey" | "SharedKeyLite"} type * @param {IRequest} req * @returns {string} * @memberof QueueSharedKeyAuthenticator */ getHeadersToSign(type, req) { if (type === "SharedKey") { return ([ req.getMethod().toUpperCase(), this.getHeaderValueToSign(req, constants_1.HeaderConstants.CONTENT_LANGUAGE), this.getHeaderValueToSign(req, constants_1.HeaderConstants.CONTENT_ENCODING), this.getHeaderValueToSign(req, constants_1.HeaderConstants.CONTENT_LENGTH), this.getHeaderValueToSign(req, constants_1.HeaderConstants.CONTENT_MD5), this.getHeaderValueToSign(req, constants_1.HeaderConstants.CONTENT_TYPE), this.getHeaderValueToSign(req, constants_1.HeaderConstants.DATE), this.getHeaderValueToSign(req, constants_1.HeaderConstants.IF_MODIFIED_SINCE), this.getHeaderValueToSign(req, constants_1.HeaderConstants.IF_MATCH), this.getHeaderValueToSign(req, constants_1.HeaderConstants.IF_NONE_MATCH), this.getHeaderValueToSign(req, constants_1.HeaderConstants.IF_UNMODIFIED_SINCE), this.getHeaderValueToSign(req, constants_1.HeaderConstants.RANGE) ].join("\n") + "\n" + this.getCanonicalizedHeadersString(req)); } else if (type === "SharedKeyLite") { return ([ req.getMethod().toUpperCase(), this.getHeaderValueToSign(req, constants_1.HeaderConstants.CONTENT_MD5), this.getHeaderValueToSign(req, constants_1.HeaderConstants.CONTENT_TYPE), this.getHeaderValueToSign(req, constants_1.HeaderConstants.DATE) ].join("\n") + "\n" + this.getCanonicalizedHeadersString(req)); } return ""; } } exports.default = QueueSharedKeyAuthenticator; //# sourceMappingURL=QueueSharedKeyAuthenticator.js.map