@graphql-hive/plugin-aws-sigv4
Version:
402 lines (397 loc) • 15.6 kB
JavaScript
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 };