Skip to content

Commit

Permalink
fix(middleware-signing): memoize temporary credentials (#2109)
Browse files Browse the repository at this point in the history
  • Loading branch information
AllanZhengYP committed Mar 11, 2021
1 parent b49c1d3 commit cf238b9
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 6 deletions.
1 change: 1 addition & 0 deletions packages/middleware-signing/package.json
Expand Up @@ -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",
Expand Down
55 changes: 55 additions & 0 deletions 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);
});
});
33 changes: 27 additions & 6 deletions 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.
Expand Down Expand Up @@ -42,9 +46,13 @@ export interface AwsAuthResolvedConfig {
signingEscapePath: boolean;
systemClockOffset: number;
}
export function resolveAwsAuthConfig<T>(input: T & AwsAuthInputConfig & PreviouslyResolved): T & AwsAuthResolvedConfig {
const credentials = input.credentials || input.credentialDefaultProvider(input as any);
const normalizedCreds = normalizeProvider(credentials);

export const resolveAwsAuthConfig = <T>(
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<RequestSigner>;
if (input.signer) {
Expand Down Expand Up @@ -81,12 +89,25 @@ export function resolveAwsAuthConfig<T>(input: T & AwsAuthInputConfig & Previous
credentials: normalizedCreds,
signer,
};
}
};

function normalizeProvider<T>(input: T | Provider<T>): Provider<T> {
const normalizeProvider = <T>(input: T | Provider<T>): Provider<T> => {
if (typeof input === "object") {
const promisified = Promise.resolve(input);
return () => promisified;
}
return input as Provider<T>;
}
};

const normalizeCredentialProvider = (credentials: Credentials | Provider<Credentials>): Provider<Credentials> => {
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);
};

0 comments on commit cf238b9

Please sign in to comment.