azurite
Version:
An open source Azure Storage API compatible server
235 lines • 13.5 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const utils_1 = require("../../common/utils/utils");
const BlobStorageContext_1 = tslib_1.__importDefault(require("../context/BlobStorageContext"));
const StorageErrorFactory_1 = tslib_1.__importDefault(require("../errors/StorageErrorFactory"));
const operation_1 = tslib_1.__importDefault(require("../generated/artifacts/operation"));
const constants_1 = require("../utils/constants");
class BlobSharedKeyAuthenticator {
constructor(dataStore, logger) {
this.dataStore = dataStore;
this.logger = logger;
}
async validate(req, context) {
const blobContext = new BlobStorageContext_1.default(context);
const account = blobContext.account;
this.logger.info(`BlobSharedKeyAuthenticator:validate() Start validation against account shared key authentication.`, blobContext.contextId);
const authHeaderValue = req.getHeader(constants_1.HeaderConstants.AUTHORIZATION);
if (authHeaderValue === undefined) {
this.logger.info(
// tslint:disable-next-line:max-line-length
`BlobSharedKeyAuthenticator:validate() Request doesn't include valid authentication header. Skip shared key authentication.`, blobContext.contextId);
return undefined;
}
else if (!authHeaderValue.startsWith("SharedKey")) {
this.logger.info(
// tslint:disable-next-line:max-line-length
`BlobSharedKeyAuthenticator:validate() Request doesn't include shared key authentication.`, blobContext.contextId);
return undefined;
}
// TODO: Make following async
const accountProperties = this.dataStore.getAccount(account);
if (accountProperties === undefined) {
this.logger.error(`BlobSharedKeyAuthenticator:validate() Invalid storage account ${account}.`, blobContext.contextId);
throw StorageErrorFactory_1.default.ResourceNotFound(blobContext.contextId);
}
const operation = context.operation;
if (operation === undefined) {
throw new Error(
// tslint:disable-next-line:max-line-length
`BlobSharedKeyAuthenticator:validate() Operation shouldn't be undefined. Please make sure DispatchMiddleware is hooked before authentication related middleware.`);
}
else if (operation === operation_1.default.Service_GetUserDelegationKey) {
this.logger.info(`BlobSharedKeyAuthenticator:validate() Service_GetUserDelegationKey requires OAuth credentials"
}.`, context.contextId);
throw StorageErrorFactory_1.default.getAuthenticationFailed(context.contextId, constants_1.AUTHENTICATION_BEARERTOKEN_REQUIRED);
}
const stringToSign = [
req.getMethod().toUpperCase(),
this.getHeaderValueToSign(req, constants_1.HeaderConstants.CONTENT_ENCODING),
this.getHeaderValueToSign(req, constants_1.HeaderConstants.CONTENT_LANGUAGE),
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) +
this.getCanonicalizedResourceString(req, account, blobContext.authenticationPath);
this.logger.info(`BlobSharedKeyAuthenticator:validate() [STRING TO SIGN]:${JSON.stringify(stringToSign)}`, blobContext.contextId);
const signature1 = (0, utils_1.computeHMACSHA256)(stringToSign, accountProperties.key1);
const authValue1 = `SharedKey ${account}:${signature1}`;
this.logger.info(`BlobSharedKeyAuthenticator:validate() Calculated authentication header based on key1: ${authValue1}`, blobContext.contextId);
if (authHeaderValue === authValue1) {
this.logger.info(`BlobSharedKeyAuthenticator:validate() Signature 1 matched.`, blobContext.contextId);
return true;
}
if (accountProperties.key2) {
const signature2 = (0, utils_1.computeHMACSHA256)(stringToSign, accountProperties.key2);
const authValue2 = `SharedKey ${account}:${signature2}`;
this.logger.info(`BlobSharedKeyAuthenticator:validate() Calculated authentication header based on key2: ${authValue2}`, blobContext.contextId);
if (authHeaderValue === authValue2) {
this.logger.info(`BlobSharedKeyAuthenticator:validate() Signature 2 matched.`, blobContext.contextId);
return true;
}
}
if (context.context.isSecondary && blobContext.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 = [
req.getMethod().toUpperCase(),
this.getHeaderValueToSign(req, constants_1.HeaderConstants.CONTENT_ENCODING),
this.getHeaderValueToSign(req, constants_1.HeaderConstants.CONTENT_LANGUAGE),
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) +
this.getCanonicalizedResourceString(req, account,
// The authenticationPath looks like "/devstoreaccount1/container", add "-secondary" after account name to "/devstoreaccount1-secondary/container"
blobContext.authenticationPath?.replace(account, account + "-secondary"));
this.logger.info(`BlobSharedKeyAuthenticator:validate() [STRING TO SIGN_secondary]:${JSON.stringify(stringToSign_secondary)}`, blobContext.contextId);
const signature1_secondary = (0, utils_1.computeHMACSHA256)(stringToSign_secondary, accountProperties.key1);
const authValue1_secondary = `SharedKey ${account}:${signature1_secondary}`;
this.logger.info(`BlobSharedKeyAuthenticator:validate() Calculated authentication header based on key1 and stringToSign with "-secondary": ${authValue1_secondary}`, blobContext.contextId);
if (authHeaderValue === authValue1_secondary) {
this.logger.info(`BlobSharedKeyAuthenticator:validate() Signature 1_secondary matched.`, blobContext.contextId);
return true;
}
if (accountProperties.key2) {
const signature2_secondary = (0, utils_1.computeHMACSHA256)(stringToSign_secondary, accountProperties.key2);
const authValue2_secondary = `SharedKey ${account}:${signature2_secondary}`;
this.logger.info(`BlobSharedKeyAuthenticator:validate() Calculated authentication header based on key2: ${authValue2_secondary}`, blobContext.contextId);
if (authHeaderValue === authValue2_secondary) {
this.logger.info(`BlobSharedKeyAuthenticator:validate() Signature 2_secondary matched.`, blobContext.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(`BlobSharedKeyAuthenticator:validate() Validation failed.`, blobContext.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;
}
/**
* Retrieves canonicalized resource string.
*
* @private
* @param {IRequest} request
* @returns {string}
* @memberof SharedKeyCredentialPolicy
*/
getCanonicalizedResourceString(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) {
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'))}`;
}
}
return canonicalizedResourceString;
}
}
exports.default = BlobSharedKeyAuthenticator;
//# sourceMappingURL=BlobSharedKeyAuthenticator.js.map
;