UNPKG

@graphql-hive/plugin-aws-sigv4

Version:
402 lines (397 loc) • 15.6 kB
import { createHmac, createHash } from 'node:crypto'; import { STS } from '@aws-sdk/client-sts'; import { subgraphNameByExecutionRequest } from '@graphql-mesh/fusion-runtime'; import { handleMaybePromise } from '@whatwg-node/promise-helpers'; import aws4 from 'aws4'; import { versionInfo, GraphQLError } from 'graphql'; const possibleGraphQLErrorProperties = [ 'message', 'locations', 'path', 'nodes', 'source', 'positions', 'originalError', 'name', 'stack', 'extensions', ]; function isGraphQLErrorLike(error) { return (error != null && typeof error === 'object' && Object.keys(error).every(key => possibleGraphQLErrorProperties.includes(key))); } function createGraphQLError(message, options) { if (options?.originalError && !(options.originalError instanceof Error) && isGraphQLErrorLike(options.originalError)) { options.originalError = createGraphQLError(options.originalError.message, options.originalError); } if (versionInfo.major >= 17) { return new GraphQLError(message, options); } return new GraphQLError(message, options?.nodes, options?.source, options?.positions, options?.path, options?.originalError, options?.extensions); } var AWSSignV4Headers = /* @__PURE__ */ ((AWSSignV4Headers2) => { AWSSignV4Headers2["Authorization"] = "authorization"; AWSSignV4Headers2["XAmzDate"] = "x-amz-date"; AWSSignV4Headers2["XAmzContentSha256"] = "x-amz-content-sha256"; AWSSignV4Headers2["XAmzExpires"] = "x-amz-expires"; return AWSSignV4Headers2; })(AWSSignV4Headers || {}); function isBufferOrString(body) { return typeof body === "string" || globalThis.Buffer?.isBuffer(body); } const DEFAULT_INCOMING_OPTIONS = { enabled: () => true, headers: (headers) => headers, secretAccessKey: () => process.env["AWS_SECRET_ACCESS_KEY"] || process.env["AWS_SECRET_KEY"], assumeRole: () => process.env["AWS_ROLE_ARN"] != null && process.env["AWS_IAM_ROLE_SESSION_NAME"] != null ? { roleArn: process.env["AWS_ROLE_ARN"], roleSessionName: process.env["AWS_IAM_ROLE_SESSION_NAME"] } : void 0, onExpired() { throw createGraphQLError("Request is expired", { extensions: { http: { status: 401 }, code: "UNAUTHORIZED" } }); }, onMissingHeaders() { throw createGraphQLError("Required headers are missing", { extensions: { http: { status: 401 }, code: "UNAUTHORIZED" } }); }, onSignatureMismatch() { throw createGraphQLError("The signature does not match", { extensions: { http: { status: 401 }, code: "UNAUTHORIZED" } }); }, onBeforeParse: () => true, onAfterParse: () => true, onSuccess() { } }; function useAWSSigv4(opts) { const outgoingOptionsFactory = typeof opts.outgoing === "function" ? opts.outgoing : () => opts.outgoing || true; const incomingOptions = opts.incoming != null && opts.incoming !== false ? opts.incoming === true ? DEFAULT_INCOMING_OPTIONS : { ...DEFAULT_INCOMING_OPTIONS, secretAccessKey(payload) { const secretFromEnv = process.env["AWS_SECRET_ACCESS_KEY"] || process.env["AWS_SECRET_KEY"]; if (secretFromEnv) { return secretFromEnv; } return handleMaybePromise( () => incomingOptions?.assumeRole?.(payload), (assumeRolePayload) => { if (!assumeRolePayload || !assumeRolePayload.roleArn || !assumeRolePayload.roleSessionName) { return; } const sts = new STS({ region: assumeRolePayload.region }); return handleMaybePromise( () => sts.assumeRole({ RoleArn: assumeRolePayload.roleArn, RoleSessionName: assumeRolePayload.roleSessionName }), (stsResult) => stsResult?.Credentials?.SecretAccessKey ); } ); }, ...opts.incoming } : void 0; return { // Handle incoming requests onRequestParse({ request, serverContext, url }) { if (incomingOptions == null) { return; } return handleMaybePromise( () => incomingOptions.enabled(request, serverContext), (ifEnabled) => { if (!ifEnabled) { return; } return handleMaybePromise( () => { if (!incomingOptions) { throw new Error("Missing options setup"); } return handleMaybePromise( () => incomingOptions.onBeforeParse(request, serverContext), (ifContinue) => { if (!ifContinue) { return; } const authorization = request.headers.get( AWSSignV4Headers.Authorization ); const xAmzDate = request.headers.get( AWSSignV4Headers.XAmzDate ); const xAmzExpires = Number( request.headers.get(AWSSignV4Headers.XAmzExpires) ); const contentSha256 = request.headers.get( AWSSignV4Headers.XAmzContentSha256 ); const bodyHash = contentSha256; if (!authorization || !xAmzDate) { return incomingOptions.onMissingHeaders?.( request, serverContext ); } let expired; if (!xAmzExpires) { expired = false; } else { const stringISO8601 = xAmzDate.replace( /^(.{4})(.{2})(.{2})T(.{2})(.{2})(.{2})Z$/, "$1-$2-$3T$4:$5:$6Z" ); const localDateTime = new Date(stringISO8601); localDateTime.setSeconds( localDateTime.getSeconds(), xAmzExpires ); expired = localDateTime < /* @__PURE__ */ new Date(); } if (expired) { return incomingOptions.onExpired?.(request, serverContext); } const [ , credentialRaw = "", signedHeadersRaw = "", _signatureRaw ] = authorization.split(/\s+/); const credential = /=([^,]*)/.exec(credentialRaw)?.[1] ?? ""; const signedHeaders = /=([^,]*)/.exec(signedHeadersRaw)?.[1] ?? ""; const [accessKey, date, region, service, requestType] = credential.split("/"); const incomingHeaders = incomingOptions.headers( request.headers ); const canonicalHeaders = signedHeaders.split(";").map( (key) => key.toLowerCase() + ":" + trimAll(incomingHeaders.get(key)) ).join("\n"); if (!accessKey || !bodyHash || !canonicalHeaders || !date || !request.method || !url.pathname || !region || !requestType || !service || !signedHeaders || !xAmzDate) { return incomingOptions.onSignatureMismatch?.( request, serverContext ); } const payload = { accessKey, authorization, bodyHash, canonicalHeaders, date, region, requestType, service, signedHeaders, xAmzDate, xAmzExpires, request, serverContext }; return handleMaybePromise( () => incomingOptions.secretAccessKey?.(payload), (secretKey) => { if (!secretKey) { return incomingOptions.onSignatureMismatch?.( request, serverContext ); } payload.secretAccessKey = secretKey; return handleMaybePromise( () => incomingOptions.onAfterParse(payload), (shouldContinue) => { if (!shouldContinue) { return; } return payload; } ); } ); } ); }, (payload) => { if (!payload) { return; } const credentialString = [ payload?.date, payload?.region, payload?.service, payload?.requestType ].join("/"); const hmacDate = hmac( "AWS4" + payload.secretAccessKey, payload.date ); const hmacRegion = hmac(hmacDate, payload.region); const hmacService = hmac(hmacRegion, payload.service); const hmacCredentials = hmac(hmacService, "aws4_request"); let canonicalURI = url.pathname; if (canonicalURI !== "/") { canonicalURI = canonicalURI.replace(/\/{2,}/g, "/"); canonicalURI = canonicalURI.split("/").reduce((_path, piece) => { if (piece === "..") { _path.pop(); } else if (piece !== ".") { _path.push(encodeRfc3986Full(piece)); } return _path; }, []).join("/"); if (canonicalURI[0] !== "/") { canonicalURI = "/" + canonicalURI; } } const reducedQuery = {}; url.searchParams.forEach((value, key) => { reducedQuery[encodeRfc3986Full(key)] = value; }); const encodedQueryPieces = []; Object.keys(reducedQuery).sort().forEach((key) => { if (!Array.isArray(reducedQuery[key])) { encodedQueryPieces.push( key + "=" + encodeRfc3986Full(reducedQuery[key] ?? "") ); } else { reducedQuery[key]?.map(encodeRfc3986Full)?.sort()?.forEach((val) => { encodedQueryPieces.push(key + "=" + val); }); } }); const canonicalQueryString = encodedQueryPieces.join("&"); const canonicalString = [ request.method, canonicalURI, canonicalQueryString, payload.canonicalHeaders + "\n", payload.signedHeaders, payload.bodyHash ].join("\n"); const stringToSign = [ "AWS4-HMAC-SHA256", payload.xAmzDate, credentialString, hash(canonicalString) ].join("\n"); const signature = hmacHex(hmacCredentials, stringToSign); const calculatedAuthorization = [ "AWS4-HMAC-SHA256 Credential=" + payload.accessKey + "/" + credentialString, "SignedHeaders=" + payload.signedHeaders, "Signature=" + signature ].join(", "); if (calculatedAuthorization !== payload?.authorization) { return incomingOptions.onSignatureMismatch?.( request, serverContext ); } return incomingOptions.onSuccess?.(payload); } ); } ); }, // Handle outgoing requests onFetch({ url, options, setURL, setOptions, executionRequest }) { const subgraphName = executionRequest && subgraphNameByExecutionRequest.get(executionRequest); if (!isBufferOrString(options.body)) { return; } const factoryResult = outgoingOptionsFactory({ url, options, subgraphName }); if (factoryResult === false) { return; } let signQuery = false; let accessKeyId = process.env["AWS_ACCESS_KEY_ID"] || process.env["AWS_ACCESS_KEY"]; let secretAccessKey = process.env["AWS_SECRET_ACCESS_KEY"] || process.env["AWS_SECRET_KEY"]; let sessionToken = process.env["AWS_SESSION_TOKEN"]; let service; let region; let roleArn = process.env["AWS_ROLE_ARN"]; let roleSessionName = process.env["AWS_IAM_ROLE_SESSION_NAME"]; if (typeof factoryResult === "object" && factoryResult != null) { signQuery = factoryResult.signQuery || false; accessKeyId = factoryResult.accessKeyId || process.env["AWS_ACCESS_KEY_ID"] || process.env["AWS_ACCESS_KEY"]; secretAccessKey = factoryResult.secretAccessKey || process.env["AWS_SECRET_ACCESS_KEY"] || process.env["AWS_SECRET_KEY"]; sessionToken = factoryResult.sessionToken || process.env["AWS_SESSION_TOKEN"]; roleArn = factoryResult.roleArn; roleSessionName = factoryResult.roleSessionName || process.env["AWS_IAM_ROLE_SESSION_NAME"]; service = factoryResult.serviceName; region = factoryResult.region; } return handleMaybePromise( () => roleArn && roleSessionName ? new STS({ region }).assumeRole({ RoleArn: roleArn, RoleSessionName: roleSessionName }) : void 0, (stsResult) => { accessKeyId = stsResult?.Credentials?.AccessKeyId || accessKeyId; secretAccessKey = stsResult?.Credentials?.SecretAccessKey || secretAccessKey; sessionToken = stsResult?.Credentials?.SessionToken || sessionToken; const parsedUrl = new URL(url); const aws4Request = { host: parsedUrl.host, method: options.method, path: `${parsedUrl.pathname}${parsedUrl.search}`, body: options.body, headers: options.headers, signQuery, service, region }; const modifiedAws4Request = aws4.sign(aws4Request, { accessKeyId, secretAccessKey, sessionToken }); setURL( `${parsedUrl.protocol}//${modifiedAws4Request.host}${modifiedAws4Request.path}` ); setOptions({ ...options, method: modifiedAws4Request.method, headers: modifiedAws4Request.headers, body: modifiedAws4Request.body }); } ); } }; } const trimAll = (header) => header?.toString().trim().replace(/\s+/g, " "); const encodeRfc3986Full = (str) => encodeRfc3986(encodeURIComponent(str)); const encodeRfc3986 = (urlEncodedString) => urlEncodedString.replace( /[!'()*]/g, (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase() ); const hmac = (secretKey, data) => createHmac("sha256", secretKey).update(data, "utf8").digest(); const hash = (data) => createHash("sha256").update(data, "utf8").digest("hex"); const hmacHex = (secretKey, data) => createHmac("sha256", secretKey).update(data, "utf8").digest("hex"); export { AWSSignV4Headers, useAWSSigv4 };