From cf238b931ee5c0514246832e9e2e36a9173e1f05 Mon Sep 17 00:00:00 2001 From: AllanZhengYP Date: Thu, 11 Mar 2021 11:08:50 -0800 Subject: [PATCH] fix(middleware-signing): memoize temporary credentials (#2109) --- packages/middleware-signing/package.json | 1 + .../src/configuration.spec.ts | 55 +++++++++++++++++++ .../middleware-signing/src/configurations.ts | 33 +++++++++-- 3 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 packages/middleware-signing/src/configuration.spec.ts diff --git a/packages/middleware-signing/package.json b/packages/middleware-signing/package.json index cae1a9adf5f8..adfcf87cdd3e 100644 --- a/packages/middleware-signing/package.json +++ b/packages/middleware-signing/package.json @@ -24,6 +24,7 @@ "typescript": "~4.1.2" }, "dependencies": { + "@aws-sdk/property-provider": "3.8.0", "@aws-sdk/protocol-http": "3.6.1", "@aws-sdk/signature-v4": "3.6.1", "@aws-sdk/types": "3.6.1", diff --git a/packages/middleware-signing/src/configuration.spec.ts b/packages/middleware-signing/src/configuration.spec.ts new file mode 100644 index 000000000000..e6fa3bc2ece5 --- /dev/null +++ b/packages/middleware-signing/src/configuration.spec.ts @@ -0,0 +1,55 @@ +import { HttpRequest } from "@aws-sdk/protocol-http"; + +import { resolveAwsAuthConfig } from "./configurations"; + +describe("resolveAwsAuthConfig", () => { + const inputParams = { + credentialDefaultProvider: () => () => Promise.resolve({ accessKeyId: "key", secretAccessKey: "secret" }), + region: jest.fn().mockImplementation(() => Promise.resolve("us-foo-1")), + regionInfoProvider: () => Promise.resolve({ hostname: "foo.com", partition: "aws" }), + serviceId: "foo", + sha256: jest.fn().mockReturnValue({ + update: jest.fn(), + digest: jest.fn().mockReturnValue("SHA256 hash"), + }), + credentials: jest.fn().mockResolvedValue({ accessKeyId: "key", secretAccessKey: "secret" }), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should memoize custom credential provider", async () => { + const { signer: signerProvider } = resolveAwsAuthConfig(inputParams); + const signer = await signerProvider(); + const request = new HttpRequest({}); + const repeats = 10; + for (let i = 0; i < repeats; i++) { + await signer.sign(request); + } + expect(inputParams.credentials).toBeCalledTimes(1); + }); + + it("should refresh custom credential provider if expired", async () => { + const FOUR_MINUTES_AND_59_SEC = 299 * 1000; + const input = { + ...inputParams, + credentials: jest + .fn() + .mockResolvedValueOnce({ + accessKeyId: "key", + secretAccessKey: "secret", + expiration: new Date(Date.now() + FOUR_MINUTES_AND_59_SEC), + }) + .mockResolvedValue({ accessKeyId: "key", secretAccessKey: "secret" }), + }; + const { signer: signerProvider } = resolveAwsAuthConfig(input); + const signer = await signerProvider(); + const request = new HttpRequest({}); + const repeats = 10; + for (let i = 0; i < repeats; i++) { + await signer.sign(request); + } + expect(input.credentials).toBeCalledTimes(2); + }); +}); diff --git a/packages/middleware-signing/src/configurations.ts b/packages/middleware-signing/src/configurations.ts index f0d03bb499c6..940b72c6bb83 100644 --- a/packages/middleware-signing/src/configurations.ts +++ b/packages/middleware-signing/src/configurations.ts @@ -1,6 +1,10 @@ +import { memoize } from "@aws-sdk/property-provider"; import { SignatureV4 } from "@aws-sdk/signature-v4"; import { Credentials, HashConstructor, Provider, RegionInfo, RegionInfoProvider, RequestSigner } from "@aws-sdk/types"; +// 5 minutes buffer time the refresh the credential before it really expires +const CREDENTIAL_EXPIRE_WINDOW = 300000; + export interface AwsAuthInputConfig { /** * The credentials used to sign requests. @@ -42,9 +46,13 @@ export interface AwsAuthResolvedConfig { signingEscapePath: boolean; systemClockOffset: number; } -export function resolveAwsAuthConfig(input: T & AwsAuthInputConfig & PreviouslyResolved): T & AwsAuthResolvedConfig { - const credentials = input.credentials || input.credentialDefaultProvider(input as any); - const normalizedCreds = normalizeProvider(credentials); + +export const resolveAwsAuthConfig = ( + input: T & AwsAuthInputConfig & PreviouslyResolved +): T & AwsAuthResolvedConfig => { + const normalizedCreds = input.credentials + ? normalizeCredentialProvider(input.credentials) + : input.credentialDefaultProvider(input as any); const { signingEscapePath = true, systemClockOffset = input.systemClockOffset || 0, sha256 } = input; let signer: Provider; if (input.signer) { @@ -81,12 +89,25 @@ export function resolveAwsAuthConfig(input: T & AwsAuthInputConfig & Previous credentials: normalizedCreds, signer, }; -} +}; -function normalizeProvider(input: T | Provider): Provider { +const normalizeProvider = (input: T | Provider): Provider => { if (typeof input === "object") { const promisified = Promise.resolve(input); return () => promisified; } return input as Provider; -} +}; + +const normalizeCredentialProvider = (credentials: Credentials | Provider): Provider => { + if (typeof credentials === "function") { + return memoize( + credentials, + (credentials) => + credentials.expiration !== undefined && + credentials.expiration.getTime() - Date.now() < CREDENTIAL_EXPIRE_WINDOW, + (credentials) => credentials.expiration !== undefined + ); + } + return normalizeProvider(credentials); +};