src/queue/authentication/AccountSASAuthenticator.ts (273 lines of code) (raw):
import IAccountDataStore from "../../common/IAccountDataStore";
import ILogger from "../../common/ILogger";
import StorageErrorFactory from "../errors/StorageErrorFactory";
import Operation from "../generated/artifacts/operation";
import Context from "../generated/Context";
import IRequest from "../generated/IRequest";
import {
generateAccountSASSignature,
IAccountSASSignatureValues
} from "../../common/authentication/IAccountSASSignatureValues";
import IAuthenticator from "./IAuthenticator";
import OPERATION_ACCOUNT_SAS_PERMISSIONS from "./OperationAccountSASPermission";
export default class AccountSASAuthenticator implements IAuthenticator {
public constructor(
private readonly accountDataStore: IAccountDataStore,
private readonly logger: ILogger
) {}
public async validate(
req: IRequest,
context: Context
): Promise<boolean | undefined> {
this.logger.info(
`AccountSASAuthenticator:validate() Start validation against account Shared Access Signature pattern.`,
context.contextID
);
this.logger.debug(
"AccountSASAuthenticator:validate() Getting account properties...",
context.contextID
);
const account: string = context.context.account;
const queueName: string | undefined = context.context.queue;
this.logger.debug(
// tslint:disable-next-line:max-line-length
`AccountSASAuthenticator:validate() Retrieved account name from context: ${account}, queue: ${queueName}`,
context.contextID
);
// TODO: Make following async
const accountProperties = this.accountDataStore.getAccount(account);
if (accountProperties === undefined) {
throw StorageErrorFactory.ResourceNotFound(
context.contextID!
);
}
this.logger.debug(
"AccountSASAuthenticator:validate() Got account properties successfully.",
context.contextID
);
const signature = this.decodeIfExist(req.getQuery("sig"));
this.logger.debug(
`AccountSASAuthenticator:validate() Retrieved signature from URL parameter sig: ${signature}`,
context.contextID
);
const values = this.getAccountSASSignatureValuesFromRequest(req);
if (values === undefined) {
this.logger.info(
`AccountSASAuthenticator:validate() Failed to get valid account SAS values from request.`,
context.contextID
);
return false;
}
this.logger.debug(
`AccountSASAuthenticator:validate() Successfully got valid account SAS values from request. ${JSON.stringify(
values
)}`,
context.contextID
);
this.logger.info(
`AccountSASAuthenticator:validate() Validate signature based account key1.`,
context.contextID
);
const [sig1, stringToSign1] = generateAccountSASSignature(
values,
account,
accountProperties.key1
);
this.logger.debug(
`AccountSASAuthenticator:validate() String to sign is: ${JSON.stringify(
stringToSign1
)}`,
context.contextID!
);
this.logger.debug(
`AccountSASAuthenticator:validate() Calculated signature is: ${sig1}`,
context.contextID!
);
const sig1Pass = sig1 === signature;
this.logger.info(
`AccountSASAuthenticator:validate() Signature based on key1 validation ${
sig1Pass ? "passed" : "failed"
}.`,
context.contextID
);
if (!sig1Pass) {
if (accountProperties.key2 === undefined) {
return false;
}
this.logger.info(
`AccountSASAuthenticator:validate() Account key2 is not empty, validate signature based account key2.`,
context.contextID
);
const [sig2, stringToSign2] = generateAccountSASSignature(
values,
account,
accountProperties.key2
);
this.logger.debug(
`AccountSASAuthenticator:validate() String to sign is: ${JSON.stringify(
stringToSign2
)}`,
context.contextID!
);
this.logger.debug(
`AccountSASAuthenticator:validate() Calculated signature is: ${sig2}`,
context.contextID!
);
const sig2Pass = sig2 === signature;
this.logger.info(
`AccountSASAuthenticator:validate() Signature based on key2 validation ${
sig2Pass ? "passed" : "failed"
}.`,
context.contextID
);
if (!sig2Pass) {
this.logger.info(
`AccountSASAuthenticator:validate() Validate signature based account key1 and key2 failed.`,
context.contextID
);
return false;
}
}
// When signature validation passes, we enforce account SAS validation
// Any validation errors will stop this request immediately
this.logger.info(
`AccountSASAuthenticator:validate() Validate start and expiry time.`,
context.contextID
);
if (!this.validateTime(values.expiryTime, values.startTime)) {
this.logger.info(
`AccountSASAuthenticator:validate() Validate start and expiry failed.`,
context.contextID
);
throw StorageErrorFactory.getAuthorizationFailure(context.contextID!);
}
this.logger.info(
`AccountSASAuthenticator:validate() Validate IP range.`,
context.contextID
);
if (!this.validateIPRange()) {
this.logger.info(
`AccountSASAuthenticator:validate() Validate IP range failed.`,
context.contextID
);
throw StorageErrorFactory.getAuthorizationSourceIPMismatch(
context.contextID!
);
}
this.logger.info(
`AccountSASAuthenticator:validate() Validate request protocol.`,
context.contextID
);
if (!this.validateProtocol(values.protocol, req.getProtocol())) {
this.logger.info(
`AccountSASAuthenticator:validate() Validate protocol failed.`,
context.contextID
);
throw StorageErrorFactory.getAuthorizationProtocolMismatch(
context.contextID!
);
}
const operation = context.operation;
if (operation === undefined) {
throw new Error(
// tslint:disable-next-line:max-line-length
`AccountSASAuthenticator:validate() operation shouldn't be undefined. Please make sure DispatchMiddleware is hooked before authentication related middleware.`
);
}
const accountSASPermission = OPERATION_ACCOUNT_SAS_PERMISSIONS.get(
operation
);
this.logger.debug(
`AccountSASAuthenticator:validate() Got permission requirements for operation ${
Operation[operation]
} - ${JSON.stringify(accountSASPermission)}`,
context.contextID
);
if (accountSASPermission === undefined) {
throw new Error(
// tslint:disable-next-line:max-line-length
`AccountSASAuthenticator:validate() OPERATION_ACCOUNT_SAS_PERMISSIONS doesn't have configuration for operation ${
Operation[operation]
}'s account SAS permission.`
);
}
if (!accountSASPermission.validateServices(values.services)) {
throw StorageErrorFactory.getAuthorizationServiceMismatch(
context.contextID!
);
}
if (!accountSASPermission.validateResourceTypes(values.resourceTypes)) {
throw StorageErrorFactory.getAuthorizationResourceTypeMismatch(
context.contextID!
);
}
if (!accountSASPermission.validatePermissions(values.permissions)) {
throw StorageErrorFactory.getAuthorizationPermissionMismatch(
context.contextID!
);
}
this.logger.info(
`AccountSASAuthenticator:validate() Account SAS validation successfully.`,
context.contextID
);
return true;
}
private getAccountSASSignatureValuesFromRequest(
req: IRequest
): IAccountSASSignatureValues | undefined {
const version = this.decodeIfExist(req.getQuery("sv"));
const services = this.decodeIfExist(req.getQuery("ss"));
const resourceTypes = this.decodeIfExist(req.getQuery("srt"));
const protocol = this.decodeIfExist(req.getQuery("spr"));
const startTime = this.decodeIfExist(req.getQuery("st"));
const expiryTime = this.decodeIfExist(req.getQuery("se"));
const ipRange = this.decodeIfExist(req.getQuery("sip"));
const permissions = this.decodeIfExist(req.getQuery("sp"));
const signature = this.decodeIfExist(req.getQuery("sig"));
if (
version === undefined ||
expiryTime === undefined ||
permissions === undefined ||
services === undefined ||
resourceTypes === undefined ||
signature === undefined
) {
return undefined;
}
const accountSASValues: IAccountSASSignatureValues = {
version,
protocol,
startTime,
expiryTime,
permissions,
ipRange,
services,
resourceTypes
};
return accountSASValues;
}
private validateTime(expiry: Date | string, start?: Date | string): boolean {
const expiryTime = new Date(expiry);
const now = new Date();
if (now > expiryTime) {
return false;
}
if (start !== undefined) {
const startTime = new Date(start);
if (now < startTime) {
return false;
}
}
return true;
}
private validateIPRange(): boolean {
// TODO: Emulator doesn't validate IP Address
return true;
}
private validateProtocol(
sasProtocol: string = "https,http",
requestProtocol: string
): boolean {
if (sasProtocol.includes(",")) {
return true;
} else {
return sasProtocol.toLowerCase() === requestProtocol;
}
}
private decodeIfExist(value?: string): string | undefined {
return value === undefined ? value : decodeURIComponent(value);
}
}