UNPKG

@graphql-mesh/hmac-upstream-signature

Version:
179 lines (175 loc) 5.77 kB
import { serializeExecutionRequest } from '@graphql-tools/executor-common'; import { handleMaybePromise } from '@whatwg-node/promise-helpers'; import jsonStableStringify from 'json-stable-stringify'; const loggerByRequest = /* @__PURE__ */ new WeakMap(); function loggerForRequest(log, request) { const reqLog = loggerByRequest.get(request); if (reqLog) { return reqLog; } loggerByRequest.set(request, log); return log; } const DEFAULT_EXTENSION_NAME = "hmac-signature"; const DEFAULT_SHOULD_SIGN_FN = () => true; const defaultExecutionRequestSerializer = (executionRequest) => jsonStableStringify( serializeExecutionRequest({ executionRequest: { document: executionRequest.document, variables: executionRequest.variables } }) ); const defaultParamsSerializer = (params) => jsonStableStringify({ query: params.query, variables: params.variables != null && Object.keys(params.variables).length > 0 ? params.variables : void 0 }); function createCryptoKey({ textEncoder, crypto, secret, usages }) { return crypto.subtle.importKey( "raw", textEncoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, usages ); } function useHmacUpstreamSignature(options) { if (!options.secret) { throw new Error( 'Property "secret" is required for useHmacUpstreamSignature plugin' ); } const shouldSign = options.shouldSign || DEFAULT_SHOULD_SIGN_FN; const extensionName = options.extensionName || DEFAULT_EXTENSION_NAME; const serializeExecutionRequest2 = options.serializeExecutionRequest || defaultExecutionRequestSerializer; let key$; let fetchAPI; let textEncoder; return { onYogaInit({ yoga }) { fetchAPI = yoga.fetchAPI; }, onSubgraphExecute({ subgraphName, subgraph, executionRequest, log: rootLog }) { const log = rootLog.child("[useHmacUpstreamSignature] "); log.debug(`Running shouldSign for subgraph ${subgraphName}`); if (shouldSign({ subgraphName, subgraph, executionRequest })) { log.debug( `shouldSign is true for subgraph ${subgraphName}, signing request` ); textEncoder ||= new fetchAPI.TextEncoder(); return handleMaybePromise( () => key$ ||= createCryptoKey({ textEncoder, crypto: fetchAPI.crypto, secret: options.secret, usages: ["sign"] }), (key) => { key$ = key; const serializedExecutionRequest = serializeExecutionRequest2(executionRequest); const encodedContent = textEncoder.encode( serializedExecutionRequest ); return handleMaybePromise( () => fetchAPI.crypto.subtle.sign("HMAC", key, encodedContent), (signature) => { const extensionValue = fetchAPI.btoa( String.fromCharCode(...new Uint8Array(signature)) ); log.debug( { signature: extensionValue, payload: serializedExecutionRequest }, `Produced hmac signature for subgraph ${subgraphName}` ); if (!executionRequest.extensions) { executionRequest.extensions = {}; } executionRequest.extensions[extensionName] = extensionValue; } ); } ); } else { log.debug( `shouldSign is false for subgraph ${subgraphName}, skipping hmac signature` ); } } }; } function useHmacSignatureValidation(options) { if (!options.secret) { throw new Error( 'Property "secret" is required for useHmacSignatureValidation plugin' ); } const extensionName = options.extensionName || DEFAULT_EXTENSION_NAME; let key$; let textEncoder; const paramsSerializer = options.serializeParams || defaultParamsSerializer; return { onParams({ params, fetchAPI, request }) { const log = loggerForRequest(options.log, request).child( "[useHmacSignatureValidation] " ); textEncoder ||= new fetchAPI.TextEncoder(); const extension = params.extensions?.[extensionName]; if (!extension) { throw new Error( `Missing HMAC signature: extension ${extensionName} not found in request.` ); } return handleMaybePromise( () => key$ ||= createCryptoKey({ textEncoder, crypto: fetchAPI.crypto, secret: options.secret, usages: ["verify"] }), (key) => { key$ = key; const sigBuf = Uint8Array.from( atob(extension), (c) => c.charCodeAt(0) ); const serializedParams = paramsSerializer(params); log.debug( { serializedParams }, "HMAC signature will be calculate based on serialized params" ); return handleMaybePromise( () => fetchAPI.crypto.subtle.verify( "HMAC", key, sigBuf, textEncoder.encode(serializedParams) ), (result) => { if (!result) { log.error( "HMAC signature does not match the body content. short circuit request." ); throw new Error( `Invalid HMAC signature: extension ${extensionName} does not match the body content.` ); } } ); } ); } }; } export { defaultExecutionRequestSerializer, defaultParamsSerializer, useHmacSignatureValidation, useHmacUpstreamSignature };