Skip to content

Commit 891e091

Browse files
authored
fix(credential-providers): supply backup credentials to fromTemporaryCredentials (#6817)
* fix(credential-providers): supply backup credentials to fromTemporaryCredentials * fix: browser file paths
1 parent d35e4ad commit 891e091

File tree

6 files changed

+193
-74
lines changed

6 files changed

+193
-74
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import type { AssumeRoleCommandInput, STSClient, STSClientConfig } from "@aws-sdk/nested-clients/sts";
2+
import type {
3+
AwsIdentityProperties,
4+
CredentialProviderOptions,
5+
RuntimeConfigAwsCredentialIdentityProvider,
6+
} from "@aws-sdk/types";
7+
import { CredentialsProviderError } from "@smithy/property-provider";
8+
import { AwsCredentialIdentity, AwsCredentialIdentityProvider, Pluggable } from "@smithy/types";
9+
10+
export interface FromTemporaryCredentialsOptions extends CredentialProviderOptions {
11+
params: Omit<AssumeRoleCommandInput, "RoleSessionName"> & { RoleSessionName?: string };
12+
masterCredentials?: AwsCredentialIdentity | AwsCredentialIdentityProvider;
13+
clientConfig?: STSClientConfig;
14+
clientPlugins?: Pluggable<any, any>[];
15+
mfaCodeProvider?: (mfaSerial: string) => Promise<string>;
16+
}
17+
18+
export const fromTemporaryCredentials = (
19+
options: FromTemporaryCredentialsOptions,
20+
credentialDefaultProvider?: () => AwsCredentialIdentityProvider
21+
): RuntimeConfigAwsCredentialIdentityProvider => {
22+
let stsClient: STSClient;
23+
return async (awsIdentityProperties: AwsIdentityProperties = {}): Promise<AwsCredentialIdentity> => {
24+
options.logger?.debug("@aws-sdk/credential-providers - fromTemporaryCredentials (STS)");
25+
const params = { ...options.params, RoleSessionName: options.params.RoleSessionName ?? "aws-sdk-js-" + Date.now() };
26+
if (params?.SerialNumber) {
27+
if (!options.mfaCodeProvider) {
28+
throw new CredentialsProviderError(
29+
`Temporary credential requires multi-factor authentication,` + ` but no MFA code callback was provided.`,
30+
{
31+
tryNextLink: false,
32+
logger: options.logger,
33+
}
34+
);
35+
}
36+
params.TokenCode = await options.mfaCodeProvider(params?.SerialNumber);
37+
}
38+
39+
const { AssumeRoleCommand, STSClient } = await import("./loadSts");
40+
41+
if (!stsClient) {
42+
const defaultCredentialsOrError =
43+
typeof credentialDefaultProvider === "function" ? credentialDefaultProvider() : undefined;
44+
45+
const { callerClientConfig } = awsIdentityProperties;
46+
stsClient = new STSClient({
47+
...options.clientConfig,
48+
credentials:
49+
options.masterCredentials ??
50+
options.clientConfig?.credentials ??
51+
callerClientConfig?.credentialDefaultProvider?.() ??
52+
defaultCredentialsOrError,
53+
});
54+
}
55+
if (options.clientPlugins) {
56+
for (const plugin of options.clientPlugins) {
57+
stsClient.middlewareStack.use(plugin);
58+
}
59+
}
60+
const { Credentials } = await stsClient.send(new AssumeRoleCommand(params));
61+
if (!Credentials || !Credentials.AccessKeyId || !Credentials.SecretAccessKey) {
62+
throw new CredentialsProviderError(`Invalid response from STS.assumeRole call with role ${params.RoleArn}`, {
63+
logger: options.logger,
64+
});
65+
}
66+
return {
67+
accessKeyId: Credentials.AccessKeyId,
68+
secretAccessKey: Credentials.SecretAccessKey,
69+
sessionToken: Credentials.SessionToken,
70+
expiration: Credentials.Expiration,
71+
// TODO(credentialScope): access normally when shape is updated.
72+
credentialScope: (Credentials as any).CredentialScope,
73+
};
74+
};
75+
};

packages/credential-providers/src/fromTemporaryCredentials.spec.ts

+96-18
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { AssumeRoleCommand, STSClient } from "@aws-sdk/nested-clients/sts";
2-
import { beforeEach, describe, expect, test as it, vi } from "vitest";
2+
import { afterEach, beforeEach, describe, expect, test as it, vi } from "vitest";
33

4-
import { fromTemporaryCredentials } from "./fromTemporaryCredentials";
4+
import { fromTemporaryCredentials as fromTemporaryCredentialsNode } from "./fromTemporaryCredentials";
5+
import { fromTemporaryCredentials } from "./fromTemporaryCredentials.browser";
56

67
const mockSend = vi.fn();
78
const mockUsePlugin = vi.fn();
@@ -55,7 +56,7 @@ describe("fromTemporaryCredentials", () => {
5556
clientConfig: { region },
5657
clientPlugins: [plugin],
5758
};
58-
const provider = fromTemporaryCredentials(options);
59+
const provider = fromTemporaryCredentialsNode(options);
5960
const credential = await provider();
6061
expect(credential).toEqual({
6162
accessKeyId: "ACCESS_KEY_ID",
@@ -77,7 +78,7 @@ describe("fromTemporaryCredentials", () => {
7778

7879
it("should create STS client if not supplied", async () => {
7980
const plugin = { applyToStack: () => {} };
80-
const provider = fromTemporaryCredentials({
81+
const provider = fromTemporaryCredentialsNode({
8182
params: {
8283
RoleArn,
8384
RoleSessionName,
@@ -93,19 +94,8 @@ describe("fromTemporaryCredentials", () => {
9394
expect(mockUsePlugin).toHaveBeenNthCalledWith(1, plugin);
9495
});
9596

96-
it("should resolve default credentials if master credential is not supplied", async () => {
97-
const provider = fromTemporaryCredentials({
98-
params: {
99-
RoleArn,
100-
RoleSessionName,
101-
},
102-
});
103-
await provider();
104-
expect(vi.mocked(STSClient as any)).toHaveBeenCalledWith({});
105-
});
106-
10797
it("should create a role session name if none provided", async () => {
108-
const provider = fromTemporaryCredentials({
98+
const provider = fromTemporaryCredentialsNode({
10999
params: { RoleArn },
110100
});
111101
await provider();
@@ -115,6 +105,94 @@ describe("fromTemporaryCredentials", () => {
115105
});
116106
});
117107

108+
describe("nested sts credential resolution order", () => {
109+
const masterCredentials = vi.fn();
110+
const clientConfigCredentials = vi.fn();
111+
const callerClientCredentials = vi.fn();
112+
const callerClientCredentialsProvider = () => callerClientCredentials;
113+
const chainCredentials = vi.fn();
114+
const chainCredentialsProvider = () => chainCredentials;
115+
116+
it("should use with 1st priority masterCredentials from the provider", async () => {
117+
const provider = fromTemporaryCredentials(
118+
{
119+
params: { RoleArn },
120+
masterCredentials: masterCredentials,
121+
clientConfig: {
122+
credentials: clientConfigCredentials,
123+
},
124+
},
125+
chainCredentialsProvider
126+
);
127+
await provider({
128+
callerClientConfig: {
129+
region: async () => "us-west-2",
130+
credentialDefaultProvider: callerClientCredentialsProvider,
131+
},
132+
});
133+
expect(masterCredentials).toHaveBeenCalled();
134+
expect(clientConfigCredentials).not.toHaveBeenCalled();
135+
expect(callerClientCredentials).not.toHaveBeenCalled();
136+
expect(chainCredentials).not.toHaveBeenCalled();
137+
});
138+
it("should use with 2nd priority options.clientConfig.credentials", async () => {
139+
const provider = fromTemporaryCredentials(
140+
{
141+
params: { RoleArn },
142+
clientConfig: {
143+
credentials: clientConfigCredentials,
144+
},
145+
},
146+
chainCredentialsProvider
147+
);
148+
await provider({
149+
callerClientConfig: {
150+
region: async () => "us-west-2",
151+
credentialDefaultProvider: callerClientCredentialsProvider,
152+
},
153+
});
154+
expect(masterCredentials).not.toHaveBeenCalled();
155+
expect(clientConfigCredentials).toHaveBeenCalled();
156+
expect(callerClientCredentials).not.toHaveBeenCalled();
157+
expect(chainCredentials).not.toHaveBeenCalled();
158+
});
159+
it("should use with 3rd priority caller client's credentialDefaultProvider", async () => {
160+
const provider = fromTemporaryCredentials(
161+
{
162+
params: { RoleArn },
163+
},
164+
chainCredentialsProvider
165+
);
166+
await provider({
167+
callerClientConfig: {
168+
region: async () => "us-west-2",
169+
credentialDefaultProvider: callerClientCredentialsProvider,
170+
},
171+
});
172+
expect(masterCredentials).not.toHaveBeenCalled();
173+
expect(clientConfigCredentials).not.toHaveBeenCalled();
174+
expect(callerClientCredentials).toHaveBeenCalled();
175+
expect(chainCredentials).not.toHaveBeenCalled();
176+
});
177+
it("should use with 4th priority the node default provider chain (if in Node.js)", async () => {
178+
const provider = fromTemporaryCredentials(
179+
{
180+
params: { RoleArn },
181+
},
182+
chainCredentialsProvider
183+
);
184+
await provider({
185+
callerClientConfig: {
186+
region: async () => "us-west-2",
187+
},
188+
});
189+
expect(masterCredentials).not.toHaveBeenCalled();
190+
expect(clientConfigCredentials).not.toHaveBeenCalled();
191+
expect(callerClientCredentials).not.toHaveBeenCalled();
192+
expect(chainCredentials).toHaveBeenCalled();
193+
});
194+
});
195+
118196
it("should allow assume roles assuming roles assuming roles ad infinitum", async () => {
119197
const roleArnOf = (id: string) => `arn:aws:iam::123456789:role/${id}`;
120198
const idOf = (roleArn: string) => roleArn.split("/")?.[1] ?? "UNKNOWN";
@@ -176,7 +254,7 @@ describe("fromTemporaryCredentials", () => {
176254
const SerialNumber = "SERIAL_NUMBER";
177255
const mfaCode = "MFA_CODE";
178256
const mfaCodeProvider = vi.fn().mockResolvedValue(mfaCode);
179-
const provider = fromTemporaryCredentials({
257+
const provider = fromTemporaryCredentialsNode({
180258
params: { RoleArn, SerialNumber, RoleSessionName },
181259
mfaCodeProvider,
182260
});
@@ -197,7 +275,7 @@ describe("fromTemporaryCredentials", () => {
197275
it("should reject the promise with a terminal error if a MFA serial presents but mfaCodeProvider is missing", async () => {
198276
const SerialNumber = "SERIAL_NUMBER";
199277
try {
200-
await fromTemporaryCredentials({
278+
await fromTemporaryCredentialsNode({
201279
params: { RoleArn, SerialNumber, RoleSessionName },
202280
})();
203281
fail("this test must fail");
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
import type { AssumeRoleCommandInput, STSClient, STSClientConfig } from "@aws-sdk/nested-clients/sts";
2-
import type { CredentialProviderOptions } from "@aws-sdk/types";
3-
import { CredentialsProviderError } from "@smithy/property-provider";
4-
import { AwsCredentialIdentity, AwsCredentialIdentityProvider, Pluggable } from "@smithy/types";
1+
import type { RuntimeConfigAwsCredentialIdentityProvider } from "@aws-sdk/types";
52

6-
export interface FromTemporaryCredentialsOptions extends CredentialProviderOptions {
7-
params: Omit<AssumeRoleCommandInput, "RoleSessionName"> & { RoleSessionName?: string };
8-
masterCredentials?: AwsCredentialIdentity | AwsCredentialIdentityProvider;
9-
clientConfig?: STSClientConfig;
10-
clientPlugins?: Pluggable<any, any>[];
11-
mfaCodeProvider?: (mfaSerial: string) => Promise<string>;
12-
}
3+
import { fromNodeProviderChain } from "./fromNodeProviderChain";
4+
import type { FromTemporaryCredentialsOptions } from "./fromTemporaryCredentials.browser";
5+
import { fromTemporaryCredentials as fromTemporaryCredentialsBase } from "./fromTemporaryCredentials.browser";
6+
7+
/**
8+
* @public
9+
*/
10+
export { FromTemporaryCredentialsOptions };
1311

1412
/**
1513
* Creates a credential provider function that retrieves temporary credentials from STS AssumeRole API.
@@ -53,45 +51,8 @@ export interface FromTemporaryCredentialsOptions extends CredentialProviderOptio
5351
*
5452
* @public
5553
*/
56-
export const fromTemporaryCredentials = (options: FromTemporaryCredentialsOptions): AwsCredentialIdentityProvider => {
57-
let stsClient: STSClient;
58-
return async (): Promise<AwsCredentialIdentity> => {
59-
options.logger?.debug("@aws-sdk/credential-providers - fromTemporaryCredentials (STS)");
60-
const params = { ...options.params, RoleSessionName: options.params.RoleSessionName ?? "aws-sdk-js-" + Date.now() };
61-
if (params?.SerialNumber) {
62-
if (!options.mfaCodeProvider) {
63-
throw new CredentialsProviderError(
64-
`Temporary credential requires multi-factor authentication,` + ` but no MFA code callback was provided.`,
65-
{
66-
tryNextLink: false,
67-
logger: options.logger,
68-
}
69-
);
70-
}
71-
params.TokenCode = await options.mfaCodeProvider(params?.SerialNumber);
72-
}
73-
74-
const { AssumeRoleCommand, STSClient } = await import("./loadSts");
75-
76-
if (!stsClient) stsClient = new STSClient({ ...options.clientConfig, credentials: options.masterCredentials });
77-
if (options.clientPlugins) {
78-
for (const plugin of options.clientPlugins) {
79-
stsClient.middlewareStack.use(plugin);
80-
}
81-
}
82-
const { Credentials } = await stsClient.send(new AssumeRoleCommand(params));
83-
if (!Credentials || !Credentials.AccessKeyId || !Credentials.SecretAccessKey) {
84-
throw new CredentialsProviderError(`Invalid response from STS.assumeRole call with role ${params.RoleArn}`, {
85-
logger: options.logger,
86-
});
87-
}
88-
return {
89-
accessKeyId: Credentials.AccessKeyId,
90-
secretAccessKey: Credentials.SecretAccessKey,
91-
sessionToken: Credentials.SessionToken,
92-
expiration: Credentials.Expiration,
93-
// TODO(credentialScope): access normally when shape is updated.
94-
credentialScope: (Credentials as any).CredentialScope,
95-
};
96-
};
54+
export const fromTemporaryCredentials = (
55+
options: FromTemporaryCredentialsOptions
56+
): RuntimeConfigAwsCredentialIdentityProvider => {
57+
return fromTemporaryCredentialsBase(options, fromNodeProviderChain);
9758
};

packages/credential-providers/src/index.browser.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ export * from "./fromCognitoIdentity";
22
export * from "./fromCognitoIdentityPool";
33
export { fromHttp } from "@aws-sdk/credential-provider-http";
44
export type { FromHttpOptions, HttpProviderCredentials } from "@aws-sdk/credential-provider-http";
5-
export * from "./fromTemporaryCredentials";
5+
export * from "./fromTemporaryCredentials.browser";
66
export * from "./fromWebToken";

packages/nested-clients/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,8 @@
8686
"dist-*/**"
8787
],
8888
"browser": {
89-
"./dist-es/nested-sso-oidc/runtimeConfig": "./dist-es/nested-sso-oidc/runtimeConfig.browser",
90-
"./dist-es/nested-sts/runtimeConfig": "./dist-es/nested-sts/runtimeConfig.browser"
89+
"./dist-es/submodules/sts/runtimeConfig": "./dist-es/submodules/sts/runtimeConfig.browser",
90+
"./dist-es/submodules/sso-oidc/runtimeConfig": "./dist-es/submodules/sso-oidc/runtimeConfig.browser"
9191
},
9292
"react-native": {},
9393
"homepage": "https://github.com/aws/aws-sdk-js-v3/tree/main/packages/nested-clients",

packages/types/src/identity/AwsCredentialIdentity.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AwsCredentialIdentity } from "@smithy/types";
1+
import type { AwsCredentialIdentity, AwsCredentialIdentityProvider } from "@smithy/types";
22

33
import type { AwsSdkCredentialsFeatures } from "../feature-ids";
44

@@ -11,6 +11,11 @@ export interface AwsIdentityProperties {
1111
callerClientConfig?: {
1212
region(): Promise<string>;
1313
profile?: string;
14+
/**
15+
* @internal
16+
* @deprecated
17+
*/
18+
credentialDefaultProvider?: (input?: any) => AwsCredentialIdentityProvider;
1419
};
1520
}
1621

0 commit comments

Comments
 (0)