@graphql-mesh/hmac-upstream-signature
Version:
103 lines (102 loc) • 5 kB
JavaScript
import jsonStableStringify from 'json-stable-stringify';
import { defaultPrintFn } from '@graphql-mesh/transport-common';
import { mapMaybePromise } from '@graphql-mesh/utils';
const DEFAULT_EXTENSION_NAME = 'hmac-signature';
const DEFAULT_SHOULD_SIGN_FN = () => true;
export const defaultExecutionRequestSerializer = (executionRequest) => jsonStableStringify({
query: defaultPrintFn(executionRequest.document),
variables: executionRequest.variables,
});
export const defaultParamsSerializer = (params) => jsonStableStringify({
query: params.query,
variables: params.variables,
});
function createCryptoKey({ textEncoder, crypto, secret, usages, }) {
return crypto.subtle.importKey('raw', textEncoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, usages);
}
export 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 serializeExecutionRequest = options.serializeExecutionRequest || defaultExecutionRequestSerializer;
let key$;
let fetchAPI;
let textEncoder;
return {
onYogaInit({ yoga }) {
fetchAPI = yoga.fetchAPI;
},
onSubgraphExecute({ subgraphName, subgraph, executionRequest, setExecutionRequest, logger }) {
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();
key$ ||= createCryptoKey({
textEncoder,
crypto: fetchAPI.crypto,
secret: options.secret,
usages: ['sign'],
});
return mapMaybePromise(key$, async (key) => {
key$ = key;
const serializedExecutionRequest = serializeExecutionRequest(executionRequest);
const encodedContent = textEncoder.encode(serializedExecutionRequest);
const signature = await fetchAPI.crypto.subtle.sign('HMAC', key, encodedContent);
const extensionValue = btoa(String.fromCharCode(...new Uint8Array(signature)));
logger.debug(`produced hmac signature for subgraph ${subgraphName}, signature: ${signature}, signed payload: ${serializedExecutionRequest}`);
setExecutionRequest({
...executionRequest,
extensions: {
...executionRequest.extensions,
[extensionName]: extensionValue,
},
});
});
}
else {
logger.debug(`shouldSign is false for subgraph ${subgraphName}, skipping hmac signature`);
}
},
};
}
export 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.`);
}
key$ ||= createCryptoKey({
textEncoder,
crypto: fetchAPI.crypto,
secret: options.secret,
usages: ['verify'],
});
return key$.then(async (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}`);
const result = await fetchAPI.crypto.subtle.verify('HMAC', key, sigBuf, textEncoder.encode(serializedParams));
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.`);
}
});
},
};
}