UNPKG

@akamai-consulting/edgegrid-js-fetch

Version:

Signing and fetch library the Akamai OPEN EdgeGrid Authentication scheme in a JS environment that supports the fetch spec

168 lines (167 loc) 5.98 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MAX_BODY = void 0; exports.createTimestamp = createTimestamp; exports.streamHash = streamHash; exports.dataToSign = dataToSign; exports.isRedirect = isRedirect; exports.base64HmacSha256 = base64HmacSha256; exports.canonicalizeHeaders = canonicalizeHeaders; exports.signingKey = signingKey; exports.signRequestWithContentHash = signRequestWithContentHash; exports.MAX_BODY = 131072; function twoDigitNumberPad(number) { return String(number).padStart(2, '0'); } /** * Create timestamp with format "yyyyMMddTHH:mm:ss+0000" * * @returns {string} Timestamp in format "yyyyMMddTHH:mm:ss+0000" */ function createTimestamp() { 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'; } /** * Converts a stream into a Uint8Array * * @param {ReadableStream} stream A stream to read * @returns {Promise<Uint8Array>} Timestamp in format "yyyyMMddTHH:mm:ss+0000" */ async function readableStreamToUint8Array(stream) { const reader = stream.getReader(); const chunks = []; let totalLength = 0; while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); totalLength += value.length; } // Concatenate chunks into one Uint8Array const result = new Uint8Array(totalLength); let offset = 0; for (const chunk of chunks) { result.set(chunk, offset); offset += chunk.length; } return result; } /** * Returns the hash of the contents of a stream * * @param {ReadableStream} stream A stream to read * @returns {Promise<string>} hash of contents of stream */ async function streamHash(stream) { const arr = await readableStreamToUint8Array(stream); if (arr.length == 0) { return ''; } const hashBuffer = await crypto.subtle.digest('SHA-256', arr); // Convert hash (ArrayBuffer) to base64 string return btoa(String.fromCharCode(...new Uint8Array(hashBuffer))); } /** * Returns data to be signed for a request * @param {Request} request The request Object to sign. * @param {String} authHeader The authorization header. * @param {String} [contentHash] The hash of the body content to include in the signature. * * @returns {Promise<String>} A Promise resolving to a string containing the data to be signed * */ async function dataToSign(request, authHeader, contentHash) { const parsedUrl = new URL(request.url), dataToSign = [ request.method.toUpperCase(), parsedUrl.protocol.replace(":", ""), parsedUrl.host, parsedUrl.pathname + parsedUrl.search, //TODO, add support for signed headers canonicalizeHeaders({}), contentHash, authHeader ]; const dataToSignStr = dataToSign.join('\t').toString(); return dataToSignStr; } const REDIRECT_CODES = [ 300, 301, 302, 303, 307 ]; /** * Determines if the http status code represents a redirect * * @param {number} statusCode The status codefrom an HTTP request * * @returns {boolean} `true` if the status code represents a redirect. Else `false` * */ function isRedirect(statusCode) { return REDIRECT_CODES.includes(statusCode); } /** * Returns an HMAC signature, using SHA256 hash algorigthm * @param {string} data The data to be signed * @param {string} key The key used to sign data * * @returns {Promise<string>} The signature * */ async function base64HmacSha256(data, key) { const encoder = new TextEncoder(); // Encode key and data as Uint8Array const keyData = encoder.encode(key); const message = encoder.encode(data); // Import the key const cryptoKey = await crypto.subtle.importKey('raw', keyData, { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign']); // Sign the message const signature = await crypto.subtle.sign('HMAC', cryptoKey, message); // Convert signature (ArrayBuffer) to base64 return btoa(String.fromCharCode(...new Uint8Array(signature))); } /** * Creates a String containing a tab delimited set of headers. * @param {Object} headers Object containing the headers to add to the set. * @returns {String} String containing a tab delimited set of headers. */ function canonicalizeHeaders(headers) { const formattedHeaders = []; let key; for (key in headers) { formattedHeaders.push(key.toLowerCase() + ':' + headers[key].trim().replace(/\s+/g, ' ')); } return formattedHeaders.join('\t'); } /** * Creates a String containing a tab delimited set of headers. * @param {string} timestamp Timestamp in format "yyyyMMddTHH:mm:ss+0000" * @param {string} clientSecret Client secret * * @returns {Promise<string>} Promise resolving to the signing key */ async function signingKey(timestamp, clientSecret) { const key = await base64HmacSha256(timestamp, clientSecret); return key; } /** * * @param {Object} request The request Object. * @param {string} 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 {String} [contentHash] The hash of the body content to include in the signature. * * @returns {Promise<string>} A Promise resolving to a string represented the requests signature */ async function signRequestWithContentHash(request, timestamp, clientSecret, authHeader, contentHash) { return await base64HmacSha256(await dataToSign(request, authHeader, contentHash), await signingKey(timestamp, clientSecret)); }