var url = require('url'), _ = require('lodash'), crypto = require('crypto'), Hawk = require('postman-request/lib/hawk'), RequestBody = require('postman-collection').RequestBody, bodyBuilder = require('../requester/core-body-builder'), urlEncoder = require('postman-url-encoder'), ASCII_SOURCE = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', ASCII_SOURCE_LENGTH = ASCII_SOURCE.length, AUTHORIZATION = 'Authorization', EMPTY = ''; /** * Generates a random string of given length (useful for nonce generation, etc). * * @param {Number} length */ function randomString (length) { length = length || 6; var result = [], i; for (i = 0; i < length; i++) { result[i] = ASCII_SOURCE[(Math.random() * ASCII_SOURCE_LENGTH) | 0]; } return result.join(EMPTY); } /** * Calculates body hash with given algorithm and digestEncoding. * REFER: https://github.com/postmanlabs/postman-request/blob/master/lib/hawk.js#L12 * * @param {RequestBody} body * @param {String} algorithm * @param {String} digestEncoding * @param {String} contentType * @param {Function} callback */ function computeBodyHash (body, algorithm, digestEncoding, contentType, callback) { if (!(body && algorithm && digestEncoding) || body.isEmpty()) { return callback(); } var hash = crypto.createHash(algorithm), originalReadStream, rawBody, urlencodedBody, graphqlBody; hash.update('hawk.1.payload\n'); hash.update((contentType ? contentType.split(';')[0].trim().toLowerCase() : '') + '\n'); if (body.mode === RequestBody.MODES.raw) { rawBody = bodyBuilder.raw(body.raw).body; hash.update(rawBody); hash.update('\n'); return callback(hash.digest(digestEncoding)); } if (body.mode === RequestBody.MODES.urlencoded) { urlencodedBody = bodyBuilder.urlencoded(body.urlencoded).form; urlencodedBody = urlEncoder.encodeQueryString(urlencodedBody); hash.update(urlencodedBody); hash.update('\n'); 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 () { hash.update('\n'); callback(hash.digest(digestEncoding)); }); }); } if (body.mode === RequestBody.MODES.graphql) { graphqlBody = bodyBuilder.graphql(body.graphql).body; hash.update(graphqlBody); hash.update('\n'); return callback(hash.digest(digestEncoding)); } // @todo: Figure out a way to calculate hash for formdata 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: 'hawk', version: '1.0.0' }, updates: [ { property: 'Authorization', type: 'header' }, { property: 'nonce', type: 'auth' }, { property: 'timestamp', type: 'auth' } ] }, /** * Initializes an item (extracts parameters from intermediate requests if any, etc) * before the actual authorization step. * * @param {AuthInterface} auth * @param {Response} response * @param {AuthHandlerInterface~authInitHookCallback} done */ init: function (auth, response, done) { done(null); }, /** * Checks the item, and fetches any parameters that are not already provided. * Sanitizes the auth parameters if needed. * * @param {AuthInterface} auth * @param {AuthHandlerInterface~authPreHookCallback} done */ pre: function (auth, done) { !auth.get('nonce') && auth.set('nonce', randomString(6)); !_.parseInt(auth.get('timestamp')) && auth.set('timestamp', Math.floor(Date.now() / 1e3)); done(null, true); }, /** * Verifies whether the request was successfully authorized after being sent. * * @param {AuthInterface} auth * @param {Response} response * @param {AuthHandlerInterface~authPostHookCallback} done */ post: function (auth, response, done) { done(null, true); }, /** * Computes signature and Auth header for a request. * * @param {Object} params * @param {Object} params.credentials Contains hawk auth credentials, "id", "key" and "algorithm" * @param {String} params.nonce * @param {String} params.ext Extra data that may be associated with the request. * @param {String} params.app Application ID used in Oz authorization protocol * @param {String} params.dlg Delegation information (used in the Oz protocol) * @param {String} params.user User id * @param {String} params.url Complete request URL * @param {String} params.method Request method * * @returns {*} */ computeHeader: function (params) { return Hawk.header(url.parse(params.url), params.method, params); }, /** * Signs a request. * * @param {AuthInterface} auth * @param {Request} request * @param {AuthHandlerInterface~authSignHookCallback} done */ sign: function (auth, request, done) { var params = auth.get([ 'authId', 'authKey', 'algorithm', 'nonce', 'timestamp', 'extraData', 'app', 'delegation', 'user', 'includePayloadHash' ]), contentType = request.headers.get('content-type'), self = this, signRequest = function (bodyHash) { // force toString to add a protocol to the URL. var url = urlEncoder.toNodeUrl(request.url), result = self.computeHeader({ credentials: { id: params.authId, key: params.authKey, algorithm: params.algorithm }, nonce: params.nonce, timestamp: params.timestamp, ext: params.extraData, app: params.app, dlg: params.delegation, user: params.user, url: url.href, method: request.method, hash: bodyHash }); request.addHeader({ key: AUTHORIZATION, value: result, system: true }); return done(); }; if (!params.authId || !params.authKey) { return done(); // Nothing to do if required parameters are not present. } request.removeHeader(AUTHORIZATION, {ignoreCase: true}); // @note: Payload verification is optional in hawk auth according to specifications (see below link). If user // opt-in for payload verification, `Content-Type` header must be specified explicitely otherwise // authentication might fail because we automatically add `Content-Type` header after auth handlers which // is not accounted while calculating payload hash for hawk auth. // documentation: https://github.com/hapijs/hawk#payload-validation // issue: https://github.com/postmanlabs/postman-app-support/issues/6550 // // @todo: Change flow of auto adding `Content-Type` header to happen before auth handlers if (!params.includePayloadHash) { return signRequest(); // sign request without calculating payload hash } computeBodyHash(request.body, params.algorithm, 'base64', contentType, signRequest); } };