Skip to content

Commit 8b6fe4c

Browse files
authored
fix(core/httpAuthSchemes): allow extensions to set signer credentials (#6971)
1 parent 052971b commit 8b6fe4c

File tree

3 files changed

+271
-42
lines changed

3 files changed

+271
-42
lines changed

Diff for: packages/core/src/submodules/httpAuthSchemes/aws_sdk/resolveAwsSdkSigV4Config.spec.ts

+74-5
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,79 @@ describe(resolveAwsSdkSigV4Config.name, () => {
4646

4747
await config.credentials(arg);
4848
expect(fn).toHaveBeenCalledWith(expect.objectContaining(arg));
49-
// todo: callerClientConfig should be `config` after https://github.com/aws/aws-sdk-js-v3/pull/6959.
50-
// expect(fn).toHaveBeenCalledWith({
51-
// ...arg,
52-
// callerClientConfig: input,
53-
// });
49+
expect(fn).toHaveBeenCalledWith({
50+
...arg,
51+
callerClientConfig: config,
52+
});
53+
});
54+
55+
it("should use a credentials getter/setter to normalize the memoization and config binding transform", async () => {
56+
const myCredentialsProvider: AwsCredentialIdentityProvider = async (arg) => {
57+
return {
58+
accessKeyId: "unit-test",
59+
secretAccessKey: "unit-test",
60+
};
61+
};
62+
63+
const input = {
64+
credentials: myCredentialsProvider,
65+
region: "us-east-1",
66+
sha256: vi.fn(),
67+
serviceId: "",
68+
useDualstackEndpoint: async () => false,
69+
useFipsEndpoint: async () => false,
70+
};
71+
72+
const config = resolveAwsSdkSigV4Config(input);
73+
74+
expect(config.credentials).not.toBe(myCredentialsProvider);
75+
expect(config.credentials.memoized).toBe(true);
76+
expect(config.credentials.configBound).toBe(true);
77+
expect(config.credentials.attributed).toBe(true);
78+
79+
// consistent getter retrieval
80+
expect(config.credentials).toBe(config.credentials);
81+
82+
// no transform applied if set to itself.
83+
const snapshot = config.credentials;
84+
config.credentials = (() => config.credentials)();
85+
expect(config.credentials).toBe(snapshot);
86+
87+
// re-normalizes input
88+
config.credentials = myCredentialsProvider;
89+
expect(config.credentials).not.toBe(myCredentialsProvider);
90+
expect(config.credentials.memoized).toBe(true);
91+
expect(config.credentials.configBound).toBe(true);
92+
expect(config.credentials.attributed).toBe(true);
93+
expect(await config.credentials()).toEqual({
94+
accessKeyId: "unit-test",
95+
secretAccessKey: "unit-test",
96+
$source: {
97+
CREDENTIALS_CODE: "e",
98+
},
99+
});
100+
101+
{
102+
// no transforms applied if they are already present according to function state variables.
103+
const fn = Object.assign(
104+
async () => {
105+
return {
106+
accessKeyId: "unit-test-2",
107+
secretAccessKey: "unit-test-2",
108+
};
109+
},
110+
{
111+
memoized: true,
112+
configBound: true,
113+
attributed: true,
114+
}
115+
) as any;
116+
config.credentials = fn;
117+
expect(config.credentials).toBe(fn);
118+
expect(await config.credentials()).toEqual({
119+
accessKeyId: "unit-test-2",
120+
secretAccessKey: "unit-test-2",
121+
});
122+
}
54123
});
55124
});

Diff for: packages/core/src/submodules/httpAuthSchemes/aws_sdk/resolveAwsSdkSigV4Config.ts

+124-36
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,25 @@ export interface AwsSdkSigV4AuthInputConfig {
5959
signerConstructor?: new (options: SignatureV4Init & SignatureV4CryptoInit) => RequestSigner;
6060
}
6161

62+
/**
63+
* Used to indicate whether a credential provider function was memoized by this resolver.
64+
* @public
65+
*/
66+
export type AwsSdkSigV4Memoized = {
67+
/**
68+
* The credential provider has been memoized by the AWS SDK SigV4 config resolver.
69+
*/
70+
memoized?: boolean;
71+
/**
72+
* The credential provider has the caller client config object bound to its arguments.
73+
*/
74+
configBound?: boolean;
75+
/**
76+
* Function is wrapped with attribution transform.
77+
*/
78+
attributed?: boolean;
79+
};
80+
6281
/**
6382
* @internal
6483
*/
@@ -82,7 +101,8 @@ export interface AwsSdkSigV4AuthResolvedConfig {
82101
* Resolved value for input config {@link AwsSdkSigV4AuthInputConfig.credentials}
83102
* This provider MAY memoize the loaded credentials for certain period.
84103
*/
85-
credentials: MergeFunctions<AwsCredentialIdentityProvider, MemoizedProvider<AwsCredentialIdentity>>;
104+
credentials: MergeFunctions<AwsCredentialIdentityProvider, MemoizedProvider<AwsCredentialIdentity>> &
105+
AwsSdkSigV4Memoized;
86106
/**
87107
* Resolved value for input config {@link AwsSdkSigV4AuthInputConfig.signer}
88108
*/
@@ -103,33 +123,42 @@ export interface AwsSdkSigV4AuthResolvedConfig {
103123
export const resolveAwsSdkSigV4Config = <T>(
104124
config: T & AwsSdkSigV4AuthInputConfig & AwsSdkSigV4PreviouslyResolved
105125
): T & AwsSdkSigV4AuthResolvedConfig => {
106-
let isUserSupplied = false;
107-
// Normalize credentials
108-
let credentialsProvider: AwsCredentialIdentityProvider | undefined;
109-
if (config.credentials) {
110-
isUserSupplied = true;
111-
credentialsProvider = memoizeIdentityProvider(config.credentials, isIdentityExpired, doesIdentityRequireRefresh);
112-
}
113-
if (!credentialsProvider) {
114-
// credentialDefaultProvider should always be populated, but in case
115-
// it isn't, set a default identity provider that throws an error
116-
if (config.credentialDefaultProvider) {
117-
credentialsProvider = normalizeProvider(
118-
config.credentialDefaultProvider(
119-
Object.assign({}, config as any, {
120-
parentClientConfig: config,
121-
})
122-
)
123-
);
124-
} else {
125-
credentialsProvider = async () => {
126-
throw new Error("`credentials` is missing");
127-
};
128-
}
129-
}
126+
let inputCredentials = config.credentials;
127+
let isUserSupplied = !!config.credentials;
128+
let resolvedCredentials: AwsSdkSigV4AuthResolvedConfig["credentials"] | undefined = undefined;
129+
130+
Object.defineProperty(config, "credentials", {
131+
set(credentials: AwsSdkSigV4AuthInputConfig["credentials"]) {
132+
if (credentials && credentials !== inputCredentials && credentials !== resolvedCredentials) {
133+
isUserSupplied = true;
134+
}
135+
inputCredentials = credentials;
136+
const memoizedProvider = normalizeCredentialProvider(config, {
137+
credentials: inputCredentials,
138+
credentialDefaultProvider: config.credentialDefaultProvider,
139+
});
140+
const boundProvider = bindCallerConfig(config, memoizedProvider);
141+
if (isUserSupplied && !boundProvider.attributed) {
142+
resolvedCredentials = async (options: Record<string, any> | undefined) =>
143+
boundProvider(options).then((creds: AttributedAwsCredentialIdentity) =>
144+
setCredentialFeature(creds, "CREDENTIALS_CODE", "e")
145+
);
146+
resolvedCredentials.memoized = boundProvider.memoized;
147+
resolvedCredentials.configBound = boundProvider.configBound;
148+
resolvedCredentials.attributed = true;
149+
} else {
150+
resolvedCredentials = boundProvider;
151+
}
152+
},
153+
get(): AwsSdkSigV4AuthResolvedConfig["credentials"] {
154+
return resolvedCredentials!;
155+
},
156+
enumerable: true,
157+
configurable: true,
158+
});
130159

131-
const boundCredentialsProvider = async (options: Record<string, any> | undefined) =>
132-
credentialsProvider!({ ...options, callerClientConfig: config });
160+
// invoke setter so that resolvedCredentials is set.
161+
config.credentials = inputCredentials;
133162

134163
// Populate sigv4 arguments
135164
const {
@@ -172,7 +201,7 @@ export const resolveAwsSdkSigV4Config = <T>(
172201

173202
const params: SignatureV4Init & SignatureV4CryptoInit = {
174203
...config,
175-
credentials: boundCredentialsProvider,
204+
credentials: config.credentials as AwsSdkSigV4AuthResolvedConfig["credentials"],
176205
region: config.signingRegion,
177206
service: config.signingName,
178207
sha256,
@@ -208,7 +237,7 @@ export const resolveAwsSdkSigV4Config = <T>(
208237

209238
const params: SignatureV4Init & SignatureV4CryptoInit = {
210239
...config,
211-
credentials: boundCredentialsProvider,
240+
credentials: config.credentials as AwsSdkSigV4AuthResolvedConfig["credentials"],
212241
region: config.signingRegion,
213242
service: config.signingName,
214243
sha256,
@@ -220,17 +249,16 @@ export const resolveAwsSdkSigV4Config = <T>(
220249
};
221250
}
222251

223-
return Object.assign(config, {
252+
const resolvedConfig = Object.assign(config, {
224253
systemClockOffset,
225254
signingEscapePath,
226-
credentials: isUserSupplied
227-
? async (options: Record<string, any> | undefined) =>
228-
boundCredentialsProvider!(options).then((creds: AttributedAwsCredentialIdentity) =>
229-
setCredentialFeature(creds, "CREDENTIALS_CODE", "e")
230-
)
231-
: boundCredentialsProvider!,
232255
signer,
233256
});
257+
258+
return resolvedConfig as typeof resolvedConfig & {
259+
// this was set earlier with Object.defineProperty.
260+
credentials: AwsSdkSigV4AuthResolvedConfig["credentials"];
261+
};
234262
};
235263

236264
/**
@@ -256,3 +284,63 @@ export interface AWSSDKSigV4AuthResolvedConfig extends AwsSdkSigV4AuthResolvedCo
256284
* @deprecated renamed to {@link resolveAwsSdkSigV4Config}
257285
*/
258286
export const resolveAWSSDKSigV4Config = resolveAwsSdkSigV4Config;
287+
288+
/**
289+
* Normalizes the credentials to a memoized provider and sets memoized=true on the function
290+
* object. This prevents multiple layering of the memoization process.
291+
*/
292+
function normalizeCredentialProvider(
293+
config: Parameters<typeof resolveAwsSdkSigV4Config>[0],
294+
{
295+
credentials,
296+
credentialDefaultProvider,
297+
}: Pick<Parameters<typeof resolveAwsSdkSigV4Config>[0], "credentials" | "credentialDefaultProvider">
298+
): AwsSdkSigV4AuthResolvedConfig["credentials"] {
299+
let credentialsProvider: AwsSdkSigV4AuthResolvedConfig["credentials"] | undefined;
300+
301+
if (credentials) {
302+
if (!(credentials as typeof credentials & AwsSdkSigV4Memoized)?.memoized) {
303+
credentialsProvider = memoizeIdentityProvider(credentials, isIdentityExpired, doesIdentityRequireRefresh)!;
304+
} else {
305+
credentialsProvider = credentials as AwsSdkSigV4AuthResolvedConfig["credentials"];
306+
}
307+
} else {
308+
// credentialDefaultProvider should always be populated, but in case
309+
// it isn't, set a default identity provider that throws an error
310+
if (credentialDefaultProvider) {
311+
credentialsProvider = normalizeProvider(
312+
credentialDefaultProvider(
313+
Object.assign({}, config as any, {
314+
parentClientConfig: config,
315+
})
316+
)
317+
);
318+
} else {
319+
credentialsProvider = async () => {
320+
throw new Error(
321+
"@aws-sdk/core::resolveAwsSdkSigV4Config - `credentials` not provided and no credentialDefaultProvider was configured."
322+
);
323+
};
324+
}
325+
}
326+
credentialsProvider.memoized = true;
327+
return credentialsProvider;
328+
}
329+
330+
/**
331+
* Binds the caller client config as an argument to the credentialsProvider function.
332+
* Uses a state marker on the function to avoid doing this more than once.
333+
*/
334+
function bindCallerConfig(
335+
config: Parameters<typeof resolveAwsSdkSigV4Config>[0],
336+
credentialsProvider: AwsSdkSigV4AuthResolvedConfig["credentials"]
337+
): AwsSdkSigV4AuthResolvedConfig["credentials"] {
338+
if (credentialsProvider.configBound) {
339+
return credentialsProvider;
340+
}
341+
const fn: typeof credentialsProvider = async (options: Parameters<typeof credentialsProvider>[0]) =>
342+
credentialsProvider({ ...options, callerClientConfig: config });
343+
fn.memoized = credentialsProvider.memoized;
344+
fn.configBound = true;
345+
return fn;
346+
}

Diff for: packages/credential-provider-node/src/credential-provider-node.integ.spec.ts

+73-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { STS } from "@aws-sdk/client-sts";
1+
import { STS, STSExtensionConfiguration } from "@aws-sdk/client-sts";
22
import * as credentialProviderHttp from "@aws-sdk/credential-provider-http";
33
import { fromCognitoIdentity, fromCognitoIdentityPool, fromIni, fromWebToken } from "@aws-sdk/credential-providers";
44
import { HttpResponse } from "@smithy/protocol-http";
@@ -1267,6 +1267,78 @@ describe("credential-provider-node integration test", () => {
12671267
});
12681268
});
12691269

1270+
describe("extension provided credentials", () => {
1271+
class OverrideCredentialsExtension {
1272+
private invocation = 0;
1273+
configure(extensionConfiguration: STSExtensionConfiguration): void {
1274+
extensionConfiguration.setCredentials(async () => ({
1275+
accessKeyId: "STS_AK" + ++this.invocation,
1276+
secretAccessKey: "STS_SAK" + this.invocation,
1277+
}));
1278+
}
1279+
}
1280+
1281+
it("allows an extension to modify client config credentials", async () => {
1282+
const client = new STS({
1283+
extensions: [new OverrideCredentialsExtension()],
1284+
});
1285+
1286+
const credentials = await client.config.credentials({});
1287+
1288+
expect(credentials).toEqual({
1289+
accessKeyId: "STS_AK1",
1290+
secretAccessKey: "STS_SAK1",
1291+
$source: {
1292+
CREDENTIALS_CODE: "e",
1293+
},
1294+
});
1295+
});
1296+
1297+
it("the extension provided credentials are still memoized", async () => {
1298+
const client = new STS({
1299+
extensions: [new OverrideCredentialsExtension()],
1300+
});
1301+
1302+
const credentials1 = await client.config.credentials({});
1303+
expect(credentials1).toEqual({
1304+
accessKeyId: "STS_AK1",
1305+
secretAccessKey: "STS_SAK1",
1306+
$source: {
1307+
CREDENTIALS_CODE: "e",
1308+
},
1309+
});
1310+
1311+
const credentials2 = await client.config.credentials({});
1312+
expect(credentials2).toEqual({
1313+
accessKeyId: "STS_AK1",
1314+
secretAccessKey: "STS_SAK1",
1315+
$source: {
1316+
CREDENTIALS_CODE: "e",
1317+
},
1318+
});
1319+
1320+
const credentials3 = await client.config.credentials({
1321+
forceRefresh: true,
1322+
});
1323+
expect(credentials3).toEqual({
1324+
accessKeyId: "STS_AK2",
1325+
secretAccessKey: "STS_SAK2",
1326+
$source: {
1327+
CREDENTIALS_CODE: "e",
1328+
},
1329+
});
1330+
1331+
const credentials4 = await client.config.credentials({});
1332+
expect(credentials4).toEqual({
1333+
accessKeyId: "STS_AK2",
1334+
secretAccessKey: "STS_SAK2",
1335+
$source: {
1336+
CREDENTIALS_CODE: "e",
1337+
},
1338+
});
1339+
});
1340+
});
1341+
12701342
describe("No credentials available", () => {
12711343
it("should throw CredentialsProviderError", async () => {
12721344
process.env.AWS_EC2_METADATA_DISABLED = "true";

0 commit comments

Comments
 (0)