Skip to content

Commit 5473e2c

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

File tree

2 files changed

+184
-37
lines changed

2 files changed

+184
-37
lines changed

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

+111-36
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ 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+
memoized?: boolean;
68+
configBound?: boolean;
69+
};
70+
6271
/**
6372
* @internal
6473
*/
@@ -82,7 +91,8 @@ export interface AwsSdkSigV4AuthResolvedConfig {
8291
* Resolved value for input config {@link AwsSdkSigV4AuthInputConfig.credentials}
8392
* This provider MAY memoize the loaded credentials for certain period.
8493
*/
85-
credentials: MergeFunctions<AwsCredentialIdentityProvider, MemoizedProvider<AwsCredentialIdentity>>;
94+
credentials: MergeFunctions<AwsCredentialIdentityProvider, MemoizedProvider<AwsCredentialIdentity>> &
95+
AwsSdkSigV4Memoized;
8696
/**
8797
* Resolved value for input config {@link AwsSdkSigV4AuthInputConfig.signer}
8898
*/
@@ -103,33 +113,39 @@ export interface AwsSdkSigV4AuthResolvedConfig {
103113
export const resolveAwsSdkSigV4Config = <T>(
104114
config: T & AwsSdkSigV4AuthInputConfig & AwsSdkSigV4PreviouslyResolved
105115
): 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-
}
116+
let inputCredentials = config.credentials;
117+
let isUserSupplied = !!config.credentials;
118+
let resolvedCredentials: AwsSdkSigV4AuthResolvedConfig["credentials"] | undefined = undefined;
130119

131-
const boundCredentialsProvider = async (options: Record<string, any> | undefined) =>
132-
credentialsProvider!({ ...options, callerClientConfig: config });
120+
Object.defineProperty(config, "credentials", {
121+
set(credentials: AwsSdkSigV4AuthInputConfig["credentials"]) {
122+
if (credentials && credentials !== inputCredentials && credentials !== resolvedCredentials) {
123+
isUserSupplied = true;
124+
}
125+
inputCredentials = credentials;
126+
const normalized = normalizeCredentialProvider(config, {
127+
credentials: inputCredentials,
128+
credentialDefaultProvider: config.credentialDefaultProvider,
129+
});
130+
const boundProvider = bindCallerConfig(config, normalized);
131+
if (isUserSupplied) {
132+
resolvedCredentials = async (options: Record<string, any> | undefined) =>
133+
boundProvider(options).then((creds: AttributedAwsCredentialIdentity) =>
134+
setCredentialFeature(creds, "CREDENTIALS_CODE", "e")
135+
);
136+
} else {
137+
resolvedCredentials = boundProvider;
138+
}
139+
},
140+
get(): AwsSdkSigV4AuthResolvedConfig["credentials"] {
141+
return resolvedCredentials!;
142+
},
143+
enumerable: true,
144+
configurable: true,
145+
});
146+
147+
// invoke setter so that resolvedCredentials is set.
148+
config.credentials = inputCredentials;
133149

134150
// Populate sigv4 arguments
135151
const {
@@ -172,7 +188,7 @@ export const resolveAwsSdkSigV4Config = <T>(
172188

173189
const params: SignatureV4Init & SignatureV4CryptoInit = {
174190
...config,
175-
credentials: boundCredentialsProvider,
191+
credentials: config.credentials as AwsSdkSigV4AuthResolvedConfig["credentials"],
176192
region: config.signingRegion,
177193
service: config.signingName,
178194
sha256,
@@ -208,7 +224,7 @@ export const resolveAwsSdkSigV4Config = <T>(
208224

209225
const params: SignatureV4Init & SignatureV4CryptoInit = {
210226
...config,
211-
credentials: boundCredentialsProvider,
227+
credentials: config.credentials as AwsSdkSigV4AuthResolvedConfig["credentials"],
212228
region: config.signingRegion,
213229
service: config.signingName,
214230
sha256,
@@ -220,19 +236,78 @@ export const resolveAwsSdkSigV4Config = <T>(
220236
};
221237
}
222238

223-
return Object.assign(config, {
239+
const resolvedConfig = Object.assign(config, {
224240
systemClockOffset,
225241
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!,
232242
signer,
233243
});
244+
245+
return resolvedConfig as typeof resolvedConfig & {
246+
// this was set earlier with Object.defineProperty.
247+
credentials: AwsSdkSigV4AuthResolvedConfig["credentials"];
248+
};
234249
};
235250

251+
/**
252+
* Normalizes the credentials to a memoized provider and sets memoized=true on the function
253+
* object. This prevents multiple layering of the memoization process.
254+
*/
255+
function normalizeCredentialProvider(
256+
config: Parameters<typeof resolveAwsSdkSigV4Config>[0],
257+
{
258+
credentials,
259+
credentialDefaultProvider,
260+
}: Pick<Parameters<typeof resolveAwsSdkSigV4Config>[0], "credentials" | "credentialDefaultProvider">
261+
): AwsSdkSigV4AuthResolvedConfig["credentials"] {
262+
let credentialsProvider: AwsSdkSigV4AuthResolvedConfig["credentials"] | undefined;
263+
264+
if (credentials) {
265+
if (!(credentials as typeof credentials & AwsSdkSigV4Memoized)?.memoized) {
266+
credentialsProvider = memoizeIdentityProvider(credentials, isIdentityExpired, doesIdentityRequireRefresh)!;
267+
credentialsProvider.memoized = true;
268+
} else {
269+
credentialsProvider = credentials as AwsSdkSigV4AuthResolvedConfig["credentials"];
270+
}
271+
} else {
272+
// credentialDefaultProvider should always be populated, but in case
273+
// it isn't, set a default identity provider that throws an error
274+
if (credentialDefaultProvider) {
275+
credentialsProvider = normalizeProvider(
276+
credentialDefaultProvider(
277+
Object.assign({}, config as any, {
278+
parentClientConfig: config,
279+
})
280+
)
281+
);
282+
} else {
283+
credentialsProvider = async () => {
284+
throw new Error(
285+
"@aws-sdk/core::resolveAwsSdkSigV4Config - `credentials` not provided and no credentialDefaultProvider was configured."
286+
);
287+
};
288+
}
289+
}
290+
return credentialsProvider;
291+
}
292+
293+
/**
294+
* Binds the caller client config as an argument to the credentialsProvider function.
295+
* Uses a state marker on the function to avoid doing this more than once.
296+
*/
297+
function bindCallerConfig(
298+
config: Parameters<typeof resolveAwsSdkSigV4Config>[0],
299+
credentialsProvider: AwsSdkSigV4AuthResolvedConfig["credentials"]
300+
): AwsSdkSigV4AuthResolvedConfig["credentials"] {
301+
if (credentialsProvider.configBound && credentialsProvider.memoized) {
302+
return credentialsProvider;
303+
}
304+
const fn: typeof credentialsProvider = async (options: Parameters<typeof credentialsProvider>[0]) =>
305+
credentialsProvider({ ...options, callerClientConfig: config });
306+
fn.memoized = credentialsProvider.memoized;
307+
fn.configBound = true;
308+
return fn;
309+
}
310+
236311
/**
237312
* @internal
238313
* @deprecated renamed to {@link AwsSdkSigV4AuthInputConfig}

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)