@graphql-mesh/hmac-upstream-signature
Version:
173 lines (170 loc) • 5.62 kB
JavaScript
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 };