UNPKG

@graphql-mesh/hmac-upstream-signature

Version:
173 lines (170 loc) 5.62 kB
import { serializeExecutionRequest } from '@graphql-tools/executor-common'; import { handleMaybePromise } from '@whatwg-node/promise-helpers'; import jsonStableStringify from 'json-stable-stringify'; 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; let yogaLogger; return { onYogaInit({ yoga }) { fetchAPI = yoga.fetchAPI; yogaLogger = yoga.logger; }, onSubgraphExecute({ subgraphName, subgraph, executionRequest, setExecutionRequest, logger = yogaLogger }) { logger?.debug(`running shouldSign for subgraph ${subgraphName}`); if (shouldSign({ subgraphName, subgraph, executionRequest })) { logger?.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)) ); logger?.debug( `produced hmac signature for subgraph ${subgraphName}, signature: ${extensionValue}, signed payload: ${serializedExecutionRequest}` ); setExecutionRequest({ ...executionRequest, extensions: { ...executionRequest.extensions, [extensionName]: extensionValue } }); } ); } ); } else { logger?.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; let logger; const paramsSerializer = options.serializeParams || defaultParamsSerializer; return { onYogaInit({ yoga }) { logger = yoga.logger; }, onParams({ params, fetchAPI }) { textEncoder ||= new fetchAPI.TextEncoder(); const extension = params.extensions?.[extensionName]; if (!extension) { logger.warn( `Missing HMAC signature: extension ${extensionName} not found in request.` ); 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); logger.debug( `HMAC signature will be calculate based on serialized params: ${serializedParams}` ); return handleMaybePromise( () => fetchAPI.crypto.subtle.verify( "HMAC", key, sigBuf, textEncoder.encode(serializedParams) ), (result) => { if (!result) { logger.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 };