UNPKG

akamai-edgegrid

Version:

Authentication handler for the Akamai OPEN EdgeGrid Authentication scheme in Node.js

211 lines (171 loc) 7.33 kB
const crypto = require('crypto'), logger = require('./logger'), path = require('path'), os = require('os'); const MAX_BODY = 131072 function twoDigitNumberPad(number) { return String(number).padStart(2, '0'); } module.exports = { /** * Create timestamp with format "yyyyMMddTHH:mm:ss+0000" * * @see https://developer.akamai.com/legacy/introduction/Client_Auth.html#authorizationheaderfields */ MAX_BODY, createTimestamp: function () { const date = new Date(Date.now()); return date.getUTCFullYear() + twoDigitNumberPad(date.getUTCMonth() + 1) + twoDigitNumberPad(date.getUTCDate()) + 'T' + twoDigitNumberPad(date.getUTCHours()) + ':' + twoDigitNumberPad(date.getUTCMinutes()) + ':' + twoDigitNumberPad(date.getUTCSeconds()) + '+0000'; }, contentHash: function (request) { let contentHash = '', preparedBody = request.body || '', isTarball = preparedBody instanceof Uint8Array && request.headers['Content-Type'] === 'application/gzip'; if (typeof preparedBody === 'object' && !isTarball) { let postDataNew = '', key; logger.info('Body content is type Object, transforming to POST data'); for (key in preparedBody) { postDataNew += key + '=' + encodeURIComponent(JSON.stringify(preparedBody[key])) + '&'; } // Strip trailing ampersand postDataNew = postDataNew.replace(/&+$/, ""); preparedBody = postDataNew; request.body = preparedBody; // Is this required or being used? } logger.info('Body is \"' + preparedBody + '\"'); logger.debug('PREPARED BODY LENGTH', preparedBody.length); if (request.method === 'POST' && preparedBody.length > 0) { logger.info('Signing content: \"' + preparedBody + '\"'); // If body data is too large, cut down to max-body size which is const value if (preparedBody.length > MAX_BODY) { logger.warn('Data length (' + preparedBody.length + ') is larger than maximum ' + MAX_BODY); if (isTarball) preparedBody = preparedBody.slice(0, MAX_BODY); else preparedBody = preparedBody.substring(0, MAX_BODY); logger.info('Body truncated. New value \"' + preparedBody + '\"'); } logger.debug('PREPARED BODY', preparedBody); contentHash = this.base64Sha256(preparedBody); logger.info('Content hash is \"' + contentHash + '\"'); } return contentHash; }, /** * * @param {Object} request The request Object. Can optionally contain a * 'headersToSign' property: An ordered list header names * that will be included in the signature. This will be * provided by specific APIs. * @param {String} authHeader The authorization header. * @param {Number} maxBody This value is deprecated. * @deprecated maxBody */ dataToSign: function (request, authHeader, maxBody) { const parsedUrl = new URL(request.url), dataToSign = [ request.method.toUpperCase(), parsedUrl.protocol.replace(":", ""), parsedUrl.host, parsedUrl.pathname + parsedUrl.search, this.canonicalizeHeaders(request.headersToSign), this.contentHash(request, maxBody), authHeader ]; const dataToSignStr = dataToSign.join('\t').toString(); logger.info('Data to sign: "' + dataToSignStr + '" \n'); return dataToSignStr; }, extend: function (a, b) { let key; for (key in b) { if (!a.hasOwnProperty(key)) { a[key] = b[key]; } } return a; }, extendHeaders: function (headers) { if (!headers.hasOwnProperty('Content-Type')) { headers['Content-Type'] = "application/json"; } if (!headers.hasOwnProperty('Accept')) { headers['Accept'] = "application/json"; } let userAgents = [headers['User-Agent']]; if (process.env['AKAMAI_CLI'] && process.env['AKAMAI_CLI_VERSION']) { userAgents.push(`AkamaiCLI/${process.env['AKAMAI_CLI_VERSION']}`); } if (process.env['AKAMAI_CLI_COMMAND'] && process.env['AKAMAI_CLI_COMMAND_VERSION']) { userAgents.push(`AkamaiCLI-${process.env['AKAMAI_CLI_COMMAND']}/${process.env['AKAMAI_CLI_COMMAND_VERSION']}`); } userAgents = userAgents.filter(v => v) if (userAgents.length > 0) { headers['User-Agent'] = userAgents.join(' ') } return headers; }, isRedirect: function (statusCode) { return [ 300, 301, 302, 303, 307 ].indexOf(statusCode) !== -1; }, base64Sha256: function (data) { const shasum = crypto.createHash('sha256').update(data); return shasum.digest('base64'); }, base64HmacSha256: function (data, key) { const encrypt = crypto.createHmac('sha256', key); encrypt.update(data); return encrypt.digest('base64'); }, /** * Creates a String containing a tab delimited set of headers. * @param {Object} headers Object containing the headers to add to the set. * @return {String} String containing a tab delimited set of headers. */ canonicalizeHeaders: function (headers) { const formattedHeaders = []; let key; for (key in headers) { formattedHeaders.push(key.toLowerCase() + ':' + headers[key].trim().replace(/\s+/g, ' ')); } return formattedHeaders.join('\t'); }, signingKey: function (timestamp, clientSecret) { const key = this.base64HmacSha256(timestamp, clientSecret); logger.info('Signing key: ' + key + '\n'); return key; }, /** * * @param {Object} request The request Object. Can optionally contain a * 'headersToSign' property: An ordered list header names * that will be included in the signature. This will be * provided by specific APIs. * @param {Date} timestamp The timestamp with format "yyyyMMddTHH:mm:ss+0000". * @param {String} clientSecret The client secret value from the .edgerc file. * @param {String} authHeader The authorization header. * @param maxBody This value is deprecated. * @returns {string} * @deprecated maxBody */ signRequest: function (request, timestamp, clientSecret, authHeader, maxBody) { return this.base64HmacSha256(this.dataToSign(request, authHeader, maxBody), this.signingKey(timestamp, clientSecret)); }, resolveHome: function (filePath) { if (filePath[0] === '~') { return path.join(os.homedir(), filePath.slice(1)); } return filePath; }, };