postman-runtime
Version:
Underlying library of executing Postman Collections
317 lines (268 loc) • 11.2 kB
JavaScript
/**
* @fileOverview
*
* Implements the EdgeGrid authentication method.
* Specification document: https://developer.akamai.com/legacy/introduction/Client_Auth.html
* Sample impletentation by Akamai: https://github.com/akamai/AkamaiOPEN-edgegrid-node
*/
var _ = require('lodash'),
uuid = require('uuid'),
crypto = require('crypto'),
sdk = require('postman-collection'),
RequestBody = sdk.RequestBody,
urlEncoder = require('postman-url-encoder'),
bodyBuilder = require('../requester/core-body-builder'),
EMPTY = '',
COLON = ':',
UTC_OFFSET = '+0000',
ZERO = '0',
DATE_TIME_SEPARATOR = 'T',
TAB = '\t',
SPACE = ' ',
SLASH = '/',
STRING = 'string',
SIGNING_ALGORITHM = 'EG1-HMAC-SHA256 ',
AUTHORIZATION = 'Authorization',
/**
* Returns current timestamp in the format described in EdgeGrid specification (yyyyMMddTHH:mm:ss+0000)
*
* @returns {String} UTC timestamp in format yyyyMMddTHH:mm:ss+0000
*/
getTimestamp = function () {
var date = new Date();
return date.getUTCFullYear() +
_.padStart(date.getUTCMonth() + 1, 2, ZERO) +
_.padStart(date.getUTCDate(), 2, ZERO) +
DATE_TIME_SEPARATOR +
_.padStart(date.getUTCHours(), 2, ZERO) +
COLON +
_.padStart(date.getUTCMinutes(), 2, ZERO) +
COLON +
_.padStart(date.getUTCSeconds(), 2, ZERO) +
UTC_OFFSET;
},
/**
* Creates a String containing a tab delimited set of headers.
*
* @param {String[]} headersToSign Headers to include in signature
* @param {Object} headers Request headers
* @returns {String} Canonicalized headers
*/
canonicalizeHeaders = function (headersToSign, headers) {
var formattedHeaders = [],
headerValue;
headersToSign.forEach(function (headerName) {
if (typeof headerName !== STRING) { return; }
// trim the header name to remove extra spaces from user input
headerName = headerName.trim().toLowerCase();
headerValue = headers[headerName];
// should not include empty headers as per the specification
if (typeof headerValue !== STRING || headerValue === EMPTY) { return; }
formattedHeaders.push(`${headerName}:${headerValue.trim().replace(/\s+/g, SPACE)}`);
});
return formattedHeaders.join(TAB);
},
/**
* Returns base64 encoding of the SHA-256 HMAC of given data signed with given key
*
* @param {String} data Data to sign
* @param {String} key Key to use while signing the data
* @returns {String} Base64 encoded signature
*/
base64HmacSha256 = function (data, key) {
var encrypt = crypto.createHmac('sha256', key);
encrypt.update(data);
return encrypt.digest('base64');
},
/**
* Calculates body hash with given algorithm and digestEncoding.
*
* @param {RequestBody} body Request body
* @param {String} algorithm Hash algorithm to use
* @param {String} digestEncoding Encoding of the hash
* @param {Function} callback Callback function that will be called with body hash
*/
computeBodyHash = function (body, algorithm, digestEncoding, callback) {
if (!(body && algorithm && digestEncoding) || body.isEmpty()) { return callback(); }
var hash = crypto.createHash(algorithm),
originalReadStream,
rawBody,
urlencodedBody,
graphqlBody;
if (body.mode === RequestBody.MODES.raw) {
rawBody = bodyBuilder.raw(body.raw).body;
hash.update(rawBody);
return callback(hash.digest(digestEncoding));
}
if (body.mode === RequestBody.MODES.urlencoded) {
urlencodedBody = bodyBuilder.urlencoded(body.urlencoded).form;
urlencodedBody = urlEncoder.encodeQueryString(urlencodedBody);
hash.update(urlencodedBody);
return callback(hash.digest(digestEncoding));
}
if (body.mode === RequestBody.MODES.file) {
originalReadStream = _.get(body, 'file.content');
if (!originalReadStream) {
return callback();
}
return originalReadStream.cloneReadStream(function (err, clonedStream) {
if (err) { return callback(); }
clonedStream.on('data', function (chunk) {
hash.update(chunk);
});
clonedStream.on('end', function () {
callback(hash.digest(digestEncoding));
});
});
}
if (body.mode === RequestBody.MODES.graphql) {
graphqlBody = bodyBuilder.graphql(body.graphql).body;
hash.update(graphqlBody);
return callback(hash.digest(digestEncoding));
}
// @todo: Figure out a way to calculate hash for form-data body type.
// ensure that callback is called if body.mode doesn't match with any of the above modes
return callback();
};
/**
* @implements {AuthHandlerInterface}
*/
module.exports = {
/**
* @property {AuthHandlerInterface~AuthManifest}
*/
manifest: {
info: {
name: 'edgegrid',
version: '1.0.0'
},
updates: [
{
property: 'Authorization',
type: 'header'
}
]
},
/**
* Initializes a item (fetches all required parameters, etc) before the actual authorization step.
*
* @param {AuthInterface} auth AuthInterface instance created with request auth
* @param {Response} response Response of intermediate request (it any)
* @param {AuthHandlerInterface~authInitHookCallback} done Callback function called with error as first argument
*/
init: function (auth, response, done) {
done(null);
},
/**
* Checks the item, and fetches any parameters that are not already provided.
*
* @param {AuthInterface} auth AuthInterface instance created with request auth
* @param {AuthHandlerInterface~authPreHookCallback} done Callback function called with error, success and request
*/
pre: function (auth, done) {
// only check required auth params here
done(null, Boolean(auth.get('accessToken') && auth.get('clientToken') && auth.get('clientSecret')));
},
/**
* Verifies whether the request was successful after being sent.
*
* @param {AuthInterface} auth AuthInterface instance created with request auth
* @param {Requester} response Response of the request
* @param {AuthHandlerInterface~authPostHookCallback} done Callback function called with error and success
*/
post: function (auth, response, done) {
done(null, true);
},
/**
* Generates the signature, and returns the Authorization header.
*
* @param {Object} params Auth parameters to use in header calculation
* @param {String} params.accessToken Access token provided by service provider
* @param {String} params.clientToken Client token provided by service provider
* @param {String} params.clientSecret Client secret provided by service provider
* @param {String} params.nonce Nonce to include in authorization header
* @param {String} params.timestamp Timestamp as defined in protocol specification
* @param {String} [params.bodyHash] Base64-encoded SHA-256 hash of request body for POST request
* @param {Object[]} params.headers Request headers
* @param {String[]} params.headersToSign Ordered list of headers to include in signature
* @param {String} params.method Request method
* @param {Url} params.url Node's URL object
* @returns {String} Authorization header
*/
computeHeader: function (params) {
var authHeader = SIGNING_ALGORITHM,
signingKey = base64HmacSha256(params.timestamp, params.clientSecret),
dataToSign;
authHeader += `client_token=${params.clientToken};`;
authHeader += `access_token=${params.accessToken};`;
authHeader += `timestamp=${params.timestamp};`;
authHeader += `nonce=${params.nonce};`;
dataToSign = [
params.method,
// trim to convert 'http:' from Node's URL object to 'http'
_.trimEnd(params.url.protocol, COLON),
params.baseURL || params.url.host,
params.url.path || SLASH,
canonicalizeHeaders(params.headersToSign, params.headers),
params.bodyHash || EMPTY,
authHeader
].join(TAB);
return authHeader + 'signature=' + base64HmacSha256(dataToSign, signingKey);
},
/**
* Signs a request.
*
* @param {AuthInterface} auth AuthInterface instance created with request auth
* @param {Request} request Request to be sent
* @param {AuthHandlerInterface~authSignHookCallback} done Callback function
*/
sign: function (auth, request, done) {
var params = auth.get([
'accessToken',
'clientToken',
'clientSecret',
'baseURL',
'nonce',
'timestamp',
'headersToSign'
]),
url = urlEncoder.toNodeUrl(request.url),
self = this;
if (!(params.accessToken && params.clientToken && params.clientSecret)) {
return done(); // Nothing to do if required parameters are not present.
}
request.removeHeader(AUTHORIZATION, { ignoreCase: true });
// Extract host from provided baseURL.
params.baseURL = params.baseURL && urlEncoder.toNodeUrl(params.baseURL).host;
params.nonce = params.nonce || uuid.v4();
params.timestamp = params.timestamp || getTimestamp();
params.url = url;
params.method = request.method;
// ensure that headers are case-insensitive as specified in the documentation
params.headers = request.getHeaders({ enabled: true, ignoreCase: true });
if (typeof params.headersToSign === STRING) {
params.headersToSign = params.headersToSign.split(',');
}
else if (!_.isArray(params.headersToSign)) {
params.headersToSign = [];
}
// only calculate body hash for POST requests according to specification
if (request.method === 'POST') {
return computeBodyHash(request.body, 'sha256', 'base64', function (bodyHash) {
params.bodyHash = bodyHash;
request.addHeader({
key: AUTHORIZATION,
value: self.computeHeader(params),
system: true
});
return done();
});
}
request.addHeader({
key: AUTHORIZATION,
value: self.computeHeader(params),
system: true
});
return done();
}
};