Skip to content

Commit f3dc887

Browse files
committed
fix(core/httpAuthSchemes): allow extensions to set signer credentials
1 parent 052971b commit f3dc887

File tree

2 files changed

+190
-37
lines changed

2 files changed

+190
-37
lines changed

packages/core/src/submodules/httpAuthSchemes/aws_sdk/resolveAwsSdkSigV4Config.ts

Lines changed: 117 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,21 @@ 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+
6277
/**
6378
* @internal
6479
*/
@@ -82,7 +97,8 @@ export interface AwsSdkSigV4AuthResolvedConfig {
8297
* Resolved value for input config {@link AwsSdkSigV4AuthInputConfig.credentials}
8398
* This provider MAY memoize the loaded credentials for certain period.
8499
*/
85-
credentials: MergeFunctions<AwsCredentialIdentityProvider, MemoizedProvider<AwsCredentialIdentity>>;
100+
credentials: MergeFunctions<AwsCredentialIdentityProvider, MemoizedProvider<AwsCredentialIdentity>> &
101+
AwsSdkSigV4Memoized;
86102
/**
87103
* Resolved value for input config {@link AwsSdkSigV4AuthInputConfig.signer}
88104
*/
@@ -103,33 +119,39 @@ export interface AwsSdkSigV4AuthResolvedConfig {
103119
export const resolveAwsSdkSigV4Config = <T>(
104120
config: T & AwsSdkSigV4AuthInputConfig & AwsSdkSigV4PreviouslyResolved
105121
): 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-
}
122+
let inputCredentials = config.credentials;
123+
let isUserSupplied = !!config.credentials;
124+
let resolvedCredentials: AwsSdkSigV4AuthResolvedConfig["credentials"] | undefined = undefined;
125+
126+
Object.defineProperty(config, "credentials", {
127+
set(credentials: AwsSdkSigV4AuthInputConfig["credentials"]) {
128+
if (credentials && credentials !== inputCredentials && credentials !== resolvedCredentials) {
129+
isUserSupplied = true;
130+
}
131+
inputCredentials = credentials;
132+
const memoizedProvider = normalizeCredentialProvider(config, {
133+
credentials: inputCredentials,
134+
credentialDefaultProvider: config.credentialDefaultProvider,
135+
});
136+
const boundProvider = bindCallerConfig(config, memoizedProvider);
137+
if (isUserSupplied) {
138+
resolvedCredentials = async (options: Record<string, any> | undefined) =>
139+
boundProvider(options).then((creds: AttributedAwsCredentialIdentity) =>
140+
setCredentialFeature(creds, "CREDENTIALS_CODE", "e")
141+
);
142+
} else {
143+
resolvedCredentials = boundProvider;
144+
}
145+
},
146+
get(): AwsSdkSigV4AuthResolvedConfig["credentials"] {
147+
return resolvedCredentials!;
148+
},
149+
enumerable: true,
150+
configurable: true,
151+
});
130152

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

134156
// Populate sigv4 arguments
135157
const {
@@ -172,7 +194,7 @@ export const resolveAwsSdkSigV4Config = <T>(
172194

173195
const params: SignatureV4Init & SignatureV4CryptoInit = {
174196
...config,
175-
credentials: boundCredentialsProvider,
197+
credentials: config.credentials as AwsSdkSigV4AuthResolvedConfig["credentials"],
176198
region: config.signingRegion,
177199
service: config.signingName,
178200
sha256,
@@ -208,7 +230,7 @@ export const resolveAwsSdkSigV4Config = <T>(
208230

209231
const params: SignatureV4Init & SignatureV4CryptoInit = {
210232
...config,
211-
credentials: boundCredentialsProvider,
233+
credentials: config.credentials as AwsSdkSigV4AuthResolvedConfig["credentials"],
212234
region: config.signingRegion,
213235
service: config.signingName,
214236
sha256,
@@ -220,17 +242,16 @@ export const resolveAwsSdkSigV4Config = <T>(
220242
};
221243
}
222244

223-
return Object.assign(config, {
245+
const resolvedConfig = Object.assign(config, {
224246
systemClockOffset,
225247
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!,
232248
signer,
233249
});
250+
251+
return resolvedConfig as typeof resolvedConfig & {
252+
// this was set earlier with Object.defineProperty.
253+
credentials: AwsSdkSigV4AuthResolvedConfig["credentials"];
254+
};
234255
};
235256

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

packages/credential-provider-node/src/credential-provider-node.integ.spec.ts

Lines changed: 73 additions & 1 deletion
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)