@filen/aws4-express
Version:
Express middleware handlers for validation AWS Signature V4
261 lines • 13.4 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.awsVerify = exports.AwsSignature = void 0;
const crypto_1 = __importDefault(require("crypto"));
const querystring_1 = __importDefault(require("querystring"));
const headers_1 = require("./headers");
class AwsSignature {
constructor() {
this.verify = (options) => (req, res, next) => __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d, _e, _f, _g;
try {
this.options = Object.assign({ enabled: () => true, headers: () => req.headers, onExpired: () => {
res.status(401).send('Request is expired');
}, onMissingHeaders: () => {
res.status(400).send('Required headers are missing');
}, onSignatureMismatch: () => {
res.status(401).send('The signature does not match');
}, onBeforeParse: () => true, onAfterParse: () => true, onSuccess: () => next() }, options);
if (yield ((_b = (_a = this.options).enabled) === null || _b === void 0 ? void 0 : _b.call(_a, req))) {
if (!(yield this.parse(req, res, next))) {
return;
}
const calculatedAuthorization = this.authHeader();
if (calculatedAuthorization !== ((_c = this.message) === null || _c === void 0 ? void 0 : _c.authorization)) {
return (_e = (_d = this.options).onSignatureMismatch) === null || _e === void 0 ? void 0 : _e.call(_d, req, res, next);
}
}
return (_g = (_f = this.options).onSuccess) === null || _g === void 0 ? void 0 : _g.call(_f, this === null || this === void 0 ? void 0 : this.message, req, res, next);
}
catch (error) {
return next(error);
}
});
this.parse = (req, res, next) => __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
if (!this.options) {
throw new Error('Missing options setup');
}
if (!(yield ((_b = (_a = this.options).onBeforeParse) === null || _b === void 0 ? void 0 : _b.call(_a, req, res, next)))) {
return false;
}
// Get the AWS signature v4 headers from the request
const authorization = req.header(headers_1.Headers.Authorization);
const xAmzDate = req.header(headers_1.Headers.XAmzDate);
const xAmzExpires = Number(req.header(headers_1.Headers.XAmzExpires));
const contentSha256 = req.header(headers_1.Headers.XAmzContentSha256);
const bodyHash = contentSha256 || this.hashBuffer((_c = req.rawBody) !== null && _c !== void 0 ? _c : '');
const { path, query } = this.parsePath(req.url);
const method = req.method;
// Check if the required headers are present
if (!authorization || !xAmzDate) {
return (_e = (_d = this.options).onMissingHeaders) === null || _e === void 0 ? void 0 : _e.call(_d, req, res, next);
}
// Expires? use xAmzExpires [seconds] to calculate
// if xAmzExpires not set will be ignored.
const expired = this.expires(xAmzDate, xAmzExpires);
if (expired) {
return yield ((_g = (_f = this.options).onExpired) === null || _g === void 0 ? void 0 : _g.call(_f, req, res, next));
}
// Extract the necessary information from the authorization header
const [, credentialRaw, signedHeadersRaw, _signatureRaw] = authorization.split(/\s+/);
const credential = (_j = (_h = /=([^,]*)/.exec(credentialRaw)) === null || _h === void 0 ? void 0 : _h[1]) !== null && _j !== void 0 ? _j : ''; // credential.split('=');
const signedHeaders = (_l = (_k = /=([^,]*)/.exec(signedHeadersRaw)) === null || _k === void 0 ? void 0 : _k[1]) !== null && _l !== void 0 ? _l : '';
const [accessKey, date, region, service, requestType] = credential.split('/');
const incommingHeaders = this.options.headers ? yield this.options.headers(req.headers) : req.headers;
const canonicalHeaders = signedHeaders
.split(';')
.map((key) => key.toLowerCase() + ':' + this.trimAll(incommingHeaders[key]))
.join('\n');
if (!accessKey ||
!bodyHash ||
!canonicalHeaders ||
!date ||
!method ||
!path ||
!region ||
!requestType ||
!service ||
!signedHeaders ||
!xAmzDate) {
yield ((_o = (_m = this.options).onSignatureMismatch) === null || _o === void 0 ? void 0 : _o.call(_m, req, res, next));
return false;
}
this.message = {
accessKey,
authorization,
bodyHash,
canonicalHeaders,
date,
method,
path,
region,
requestType,
query,
service,
signedHeaders,
xAmzDate,
xAmzExpires,
};
this.secretKey = yield this.options.secretKey(this.message, req, res, next);
if (!this.secretKey) {
yield ((_q = (_p = this.options).onSignatureMismatch) === null || _q === void 0 ? void 0 : _q.call(_p, req, res, next));
return false;
}
if (!(yield ((_s = (_r = this.options).onAfterParse) === null || _s === void 0 ? void 0 : _s.call(_r, this.message, req, res, next)))) {
return false;
}
return true;
});
this.authHeader = () => {
if (!this.message) {
throw new Error('Missing parsed incoming message');
}
return [
'AWS4-HMAC-SHA256 Credential=' + this.message.accessKey + '/' + this.credentialString(),
'SignedHeaders=' + this.message.signedHeaders,
'Signature=' + this.signature(),
].join(', ');
};
this.credentialString = () => {
var _a, _b, _c, _d;
if (!this.message) {
throw new Error('Missing parsed incoming message');
}
return [(_a = this.message) === null || _a === void 0 ? void 0 : _a.date, (_b = this.message) === null || _b === void 0 ? void 0 : _b.region, (_c = this.message) === null || _c === void 0 ? void 0 : _c.service, (_d = this.message) === null || _d === void 0 ? void 0 : _d.requestType].join('/');
};
this.signature = () => {
if (!this.message || !this.secretKey) {
throw new Error('Missing parsed incoming message');
}
const hmacDate = this.hmac('AWS4' + this.secretKey, this.message.date);
const hmacRegion = this.hmac(hmacDate, this.message.region);
const hmacService = this.hmac(hmacRegion, this.message.service);
const hmacCredentials = this.hmac(hmacService, 'aws4_request');
return this.hmacHex(hmacCredentials, this.stringToSign());
};
this.stringToSign = () => {
if (!this.message) {
throw new Error('Missing parsed incoming message');
}
return ['AWS4-HMAC-SHA256', this.message.xAmzDate, this.credentialString(), this.hash(this.canonicalString())].join('\n');
};
this.canonicalString = () => {
if (!this.message) {
throw new Error('Missing parsed incoming message');
}
return [
this.message.method,
this.canonicalURI(),
this.canonicalQueryString(),
this.message.canonicalHeaders + '\n',
this.message.signedHeaders,
this.message.bodyHash,
].join('\n');
};
this.parsePath = (url) => {
let path = url || '/';
if (/[^0-9A-Za-z;,/?:@&=+$\-_.!~*'()#%]/.test(path)) {
path = encodeURI(decodeURI(path));
}
const queryIx = path.indexOf('?');
let query;
if (queryIx >= 0) {
query = querystring_1.default.parse(path.slice(queryIx + 1));
path = path.slice(0, queryIx);
}
return {
path,
query,
};
};
this.canonicalQueryString = () => {
if (!this.message) {
throw new Error('Missing parsed incoming message');
}
if (!this.message.query) {
return '';
}
const reducedQuery = Object.keys(this.message.query).reduce((obj, key) => {
var _a, _b;
if (!key) {
return obj;
}
obj[this.encodeRfc3986Full(key)] = (_b = (_a = this.message) === null || _a === void 0 ? void 0 : _a.query) === null || _b === void 0 ? void 0 : _b[key];
return obj;
}, {});
const encodedQueryPieces = [];
Object.keys(reducedQuery)
.sort()
.forEach((key) => {
var _a, _b, _c, _d;
if (!Array.isArray(reducedQuery[key])) {
encodedQueryPieces.push(key + '=' + this.encodeRfc3986Full((_a = reducedQuery[key]) !== null && _a !== void 0 ? _a : ''));
}
else {
(_d = (_c = (_b = reducedQuery[key]) === null || _b === void 0 ? void 0 : _b.map(this.encodeRfc3986Full)) === null || _c === void 0 ? void 0 : _c.sort()) === null || _d === void 0 ? void 0 : _d.forEach((val) => {
encodedQueryPieces.push(key + '=' + val);
});
}
});
return encodedQueryPieces.join('&');
};
this.canonicalURI = () => {
if (!this.message) {
throw new Error('Missing parsed incoming message');
}
let pathStr = this.message.path;
if (pathStr !== '/') {
pathStr = pathStr.replace(/\/{2,}/g, '/');
pathStr = pathStr
.split('/')
.reduce((_path, piece) => {
if (piece === '..') {
_path.pop();
}
else if (piece !== '.') {
_path.push(this.encodeRfc3986Full(piece));
}
return _path;
}, [])
.join('/');
if (pathStr[0] !== '/') {
pathStr = '/' + pathStr;
}
}
return pathStr;
};
this.trimAll = (header) => header === null || header === void 0 ? void 0 : header.toString().trim().replace(/\s+/g, ' ');
this.encodeRfc3986 = (urlEncodedString) => urlEncodedString.replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase());
this.encodeRfc3986Full = (str) => this.encodeRfc3986(encodeURIComponent(str));
this.hmacHex = (secretKey, data) => crypto_1.default.createHmac('sha256', secretKey).update(data, 'utf8').digest('hex');
this.hmac = (secretKey, data) => crypto_1.default.createHmac('sha256', secretKey).update(data, 'utf8').digest();
this.hash = (data) => crypto_1.default.createHash('sha256').update(data, 'utf8').digest('hex');
this.hashBuffer = (data) => crypto_1.default.createHash('sha256').update(data).digest('hex');
this.expires = (dateTime, expires) => {
if (!expires) {
return false;
}
const stringISO8601 = dateTime.replace(/^(.{4})(.{2})(.{2})T(.{2})(.{2})(.{2})Z$/, '$1-$2-$3T$4:$5:$6Z');
const localDateTime = new Date(stringISO8601);
localDateTime.setSeconds(localDateTime.getSeconds(), expires);
return localDateTime < new Date();
};
}
}
exports.AwsSignature = AwsSignature;
const awsVerify = (options) => new AwsSignature().verify(options);
exports.awsVerify = awsVerify;
//# sourceMappingURL=awsSignature.js.map