Skip to content

Commit cf238b9

Browse files
authored
fix(middleware-signing): memoize temporary credentials (#2109)
1 parent b49c1d3 commit cf238b9

File tree

3 files changed

+83
-6
lines changed

3 files changed

+83
-6
lines changed

packages/middleware-signing/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"typescript": "~4.1.2"
2525
},
2626
"dependencies": {
27+
"@aws-sdk/property-provider": "3.8.0",
2728
"@aws-sdk/protocol-http": "3.6.1",
2829
"@aws-sdk/signature-v4": "3.6.1",
2930
"@aws-sdk/types": "3.6.1",
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { HttpRequest } from "@aws-sdk/protocol-http";
2+
3+
import { resolveAwsAuthConfig } from "./configurations";
4+
5+
describe("resolveAwsAuthConfig", () => {
6+
const inputParams = {
7+
credentialDefaultProvider: () => () => Promise.resolve({ accessKeyId: "key", secretAccessKey: "secret" }),
8+
region: jest.fn().mockImplementation(() => Promise.resolve("us-foo-1")),
9+
regionInfoProvider: () => Promise.resolve({ hostname: "foo.com", partition: "aws" }),
10+
serviceId: "foo",
11+
sha256: jest.fn().mockReturnValue({
12+
update: jest.fn(),
13+
digest: jest.fn().mockReturnValue("SHA256 hash"),
14+
}),
15+
credentials: jest.fn().mockResolvedValue({ accessKeyId: "key", secretAccessKey: "secret" }),
16+
};
17+
18+
beforeEach(() => {
19+
jest.clearAllMocks();
20+
});
21+
22+
it("should memoize custom credential provider", async () => {
23+
const { signer: signerProvider } = resolveAwsAuthConfig(inputParams);
24+
const signer = await signerProvider();
25+
const request = new HttpRequest({});
26+
const repeats = 10;
27+
for (let i = 0; i < repeats; i++) {
28+
await signer.sign(request);
29+
}
30+
expect(inputParams.credentials).toBeCalledTimes(1);
31+
});
32+
33+
it("should refresh custom credential provider if expired", async () => {
34+
const FOUR_MINUTES_AND_59_SEC = 299 * 1000;
35+
const input = {
36+
...inputParams,
37+
credentials: jest
38+
.fn()
39+
.mockResolvedValueOnce({
40+
accessKeyId: "key",
41+
secretAccessKey: "secret",
42+
expiration: new Date(Date.now() + FOUR_MINUTES_AND_59_SEC),
43+
})
44+
.mockResolvedValue({ accessKeyId: "key", secretAccessKey: "secret" }),
45+
};
46+
const { signer: signerProvider } = resolveAwsAuthConfig(input);
47+
const signer = await signerProvider();
48+
const request = new HttpRequest({});
49+
const repeats = 10;
50+
for (let i = 0; i < repeats; i++) {
51+
await signer.sign(request);
52+
}
53+
expect(input.credentials).toBeCalledTimes(2);
54+
});
55+
});

packages/middleware-signing/src/configurations.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import { memoize } from "@aws-sdk/property-provider";
12
import { SignatureV4 } from "@aws-sdk/signature-v4";
23
import { Credentials, HashConstructor, Provider, RegionInfo, RegionInfoProvider, RequestSigner } from "@aws-sdk/types";
34

5+
// 5 minutes buffer time the refresh the credential before it really expires
6+
const CREDENTIAL_EXPIRE_WINDOW = 300000;
7+
48
export interface AwsAuthInputConfig {
59
/**
610
* The credentials used to sign requests.
@@ -42,9 +46,13 @@ export interface AwsAuthResolvedConfig {
4246
signingEscapePath: boolean;
4347
systemClockOffset: number;
4448
}
45-
export function resolveAwsAuthConfig<T>(input: T & AwsAuthInputConfig & PreviouslyResolved): T & AwsAuthResolvedConfig {
46-
const credentials = input.credentials || input.credentialDefaultProvider(input as any);
47-
const normalizedCreds = normalizeProvider(credentials);
49+
50+
export const resolveAwsAuthConfig = <T>(
51+
input: T & AwsAuthInputConfig & PreviouslyResolved
52+
): T & AwsAuthResolvedConfig => {
53+
const normalizedCreds = input.credentials
54+
? normalizeCredentialProvider(input.credentials)
55+
: input.credentialDefaultProvider(input as any);
4856
const { signingEscapePath = true, systemClockOffset = input.systemClockOffset || 0, sha256 } = input;
4957
let signer: Provider<RequestSigner>;
5058
if (input.signer) {
@@ -81,12 +89,25 @@ export function resolveAwsAuthConfig<T>(input: T & AwsAuthInputConfig & Previous
8189
credentials: normalizedCreds,
8290
signer,
8391
};
84-
}
92+
};
8593

86-
function normalizeProvider<T>(input: T | Provider<T>): Provider<T> {
94+
const normalizeProvider = <T>(input: T | Provider<T>): Provider<T> => {
8795
if (typeof input === "object") {
8896
const promisified = Promise.resolve(input);
8997
return () => promisified;
9098
}
9199
return input as Provider<T>;
92-
}
100+
};
101+
102+
const normalizeCredentialProvider = (credentials: Credentials | Provider<Credentials>): Provider<Credentials> => {
103+
if (typeof credentials === "function") {
104+
return memoize(
105+
credentials,
106+
(credentials) =>
107+
credentials.expiration !== undefined &&
108+
credentials.expiration.getTime() - Date.now() < CREDENTIAL_EXPIRE_WINDOW,
109+
(credentials) => credentials.expiration !== undefined
110+
);
111+
}
112+
return normalizeProvider(credentials);
113+
};

0 commit comments

Comments
 (0)