diff --git a/packages/credential-providers/src/fromTemporaryCredentials.browser.ts b/packages/credential-providers/src/fromTemporaryCredentials.browser.ts new file mode 100644 index 000000000000..06d14f9a0c70 --- /dev/null +++ b/packages/credential-providers/src/fromTemporaryCredentials.browser.ts @@ -0,0 +1,75 @@ +import type { AssumeRoleCommandInput, STSClient, STSClientConfig } from "@aws-sdk/nested-clients/sts"; +import type { + AwsIdentityProperties, + CredentialProviderOptions, + RuntimeConfigAwsCredentialIdentityProvider, +} from "@aws-sdk/types"; +import { CredentialsProviderError } from "@smithy/property-provider"; +import { AwsCredentialIdentity, AwsCredentialIdentityProvider, Pluggable } from "@smithy/types"; + +export interface FromTemporaryCredentialsOptions extends CredentialProviderOptions { + params: Omit & { RoleSessionName?: string }; + masterCredentials?: AwsCredentialIdentity | AwsCredentialIdentityProvider; + clientConfig?: STSClientConfig; + clientPlugins?: Pluggable[]; + mfaCodeProvider?: (mfaSerial: string) => Promise; +} + +export const fromTemporaryCredentials = ( + options: FromTemporaryCredentialsOptions, + credentialDefaultProvider?: () => AwsCredentialIdentityProvider +): RuntimeConfigAwsCredentialIdentityProvider => { + let stsClient: STSClient; + return async (awsIdentityProperties: AwsIdentityProperties = {}): Promise => { + options.logger?.debug("@aws-sdk/credential-providers - fromTemporaryCredentials (STS)"); + const params = { ...options.params, RoleSessionName: options.params.RoleSessionName ?? "aws-sdk-js-" + Date.now() }; + if (params?.SerialNumber) { + if (!options.mfaCodeProvider) { + throw new CredentialsProviderError( + `Temporary credential requires multi-factor authentication,` + ` but no MFA code callback was provided.`, + { + tryNextLink: false, + logger: options.logger, + } + ); + } + params.TokenCode = await options.mfaCodeProvider(params?.SerialNumber); + } + + const { AssumeRoleCommand, STSClient } = await import("./loadSts"); + + if (!stsClient) { + const defaultCredentialsOrError = + typeof credentialDefaultProvider === "function" ? credentialDefaultProvider() : undefined; + + const { callerClientConfig } = awsIdentityProperties; + stsClient = new STSClient({ + ...options.clientConfig, + credentials: + options.masterCredentials ?? + options.clientConfig?.credentials ?? + callerClientConfig?.credentialDefaultProvider?.() ?? + defaultCredentialsOrError, + }); + } + if (options.clientPlugins) { + for (const plugin of options.clientPlugins) { + stsClient.middlewareStack.use(plugin); + } + } + const { Credentials } = await stsClient.send(new AssumeRoleCommand(params)); + if (!Credentials || !Credentials.AccessKeyId || !Credentials.SecretAccessKey) { + throw new CredentialsProviderError(`Invalid response from STS.assumeRole call with role ${params.RoleArn}`, { + logger: options.logger, + }); + } + return { + accessKeyId: Credentials.AccessKeyId, + secretAccessKey: Credentials.SecretAccessKey, + sessionToken: Credentials.SessionToken, + expiration: Credentials.Expiration, + // TODO(credentialScope): access normally when shape is updated. + credentialScope: (Credentials as any).CredentialScope, + }; + }; +}; diff --git a/packages/credential-providers/src/fromTemporaryCredentials.spec.ts b/packages/credential-providers/src/fromTemporaryCredentials.spec.ts index c5ce99b20e2b..ea0f512e63a8 100644 --- a/packages/credential-providers/src/fromTemporaryCredentials.spec.ts +++ b/packages/credential-providers/src/fromTemporaryCredentials.spec.ts @@ -1,7 +1,8 @@ import { AssumeRoleCommand, STSClient } from "@aws-sdk/nested-clients/sts"; -import { beforeEach, describe, expect, test as it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, test as it, vi } from "vitest"; -import { fromTemporaryCredentials } from "./fromTemporaryCredentials"; +import { fromTemporaryCredentials as fromTemporaryCredentialsNode } from "./fromTemporaryCredentials"; +import { fromTemporaryCredentials } from "./fromTemporaryCredentials.browser"; const mockSend = vi.fn(); const mockUsePlugin = vi.fn(); @@ -55,7 +56,7 @@ describe("fromTemporaryCredentials", () => { clientConfig: { region }, clientPlugins: [plugin], }; - const provider = fromTemporaryCredentials(options); + const provider = fromTemporaryCredentialsNode(options); const credential = await provider(); expect(credential).toEqual({ accessKeyId: "ACCESS_KEY_ID", @@ -77,7 +78,7 @@ describe("fromTemporaryCredentials", () => { it("should create STS client if not supplied", async () => { const plugin = { applyToStack: () => {} }; - const provider = fromTemporaryCredentials({ + const provider = fromTemporaryCredentialsNode({ params: { RoleArn, RoleSessionName, @@ -93,19 +94,8 @@ describe("fromTemporaryCredentials", () => { expect(mockUsePlugin).toHaveBeenNthCalledWith(1, plugin); }); - it("should resolve default credentials if master credential is not supplied", async () => { - const provider = fromTemporaryCredentials({ - params: { - RoleArn, - RoleSessionName, - }, - }); - await provider(); - expect(vi.mocked(STSClient as any)).toHaveBeenCalledWith({}); - }); - it("should create a role session name if none provided", async () => { - const provider = fromTemporaryCredentials({ + const provider = fromTemporaryCredentialsNode({ params: { RoleArn }, }); await provider(); @@ -115,6 +105,94 @@ describe("fromTemporaryCredentials", () => { }); }); + describe("nested sts credential resolution order", () => { + const masterCredentials = vi.fn(); + const clientConfigCredentials = vi.fn(); + const callerClientCredentials = vi.fn(); + const callerClientCredentialsProvider = () => callerClientCredentials; + const chainCredentials = vi.fn(); + const chainCredentialsProvider = () => chainCredentials; + + it("should use with 1st priority masterCredentials from the provider", async () => { + const provider = fromTemporaryCredentials( + { + params: { RoleArn }, + masterCredentials: masterCredentials, + clientConfig: { + credentials: clientConfigCredentials, + }, + }, + chainCredentialsProvider + ); + await provider({ + callerClientConfig: { + region: async () => "us-west-2", + credentialDefaultProvider: callerClientCredentialsProvider, + }, + }); + expect(masterCredentials).toHaveBeenCalled(); + expect(clientConfigCredentials).not.toHaveBeenCalled(); + expect(callerClientCredentials).not.toHaveBeenCalled(); + expect(chainCredentials).not.toHaveBeenCalled(); + }); + it("should use with 2nd priority options.clientConfig.credentials", async () => { + const provider = fromTemporaryCredentials( + { + params: { RoleArn }, + clientConfig: { + credentials: clientConfigCredentials, + }, + }, + chainCredentialsProvider + ); + await provider({ + callerClientConfig: { + region: async () => "us-west-2", + credentialDefaultProvider: callerClientCredentialsProvider, + }, + }); + expect(masterCredentials).not.toHaveBeenCalled(); + expect(clientConfigCredentials).toHaveBeenCalled(); + expect(callerClientCredentials).not.toHaveBeenCalled(); + expect(chainCredentials).not.toHaveBeenCalled(); + }); + it("should use with 3rd priority caller client's credentialDefaultProvider", async () => { + const provider = fromTemporaryCredentials( + { + params: { RoleArn }, + }, + chainCredentialsProvider + ); + await provider({ + callerClientConfig: { + region: async () => "us-west-2", + credentialDefaultProvider: callerClientCredentialsProvider, + }, + }); + expect(masterCredentials).not.toHaveBeenCalled(); + expect(clientConfigCredentials).not.toHaveBeenCalled(); + expect(callerClientCredentials).toHaveBeenCalled(); + expect(chainCredentials).not.toHaveBeenCalled(); + }); + it("should use with 4th priority the node default provider chain (if in Node.js)", async () => { + const provider = fromTemporaryCredentials( + { + params: { RoleArn }, + }, + chainCredentialsProvider + ); + await provider({ + callerClientConfig: { + region: async () => "us-west-2", + }, + }); + expect(masterCredentials).not.toHaveBeenCalled(); + expect(clientConfigCredentials).not.toHaveBeenCalled(); + expect(callerClientCredentials).not.toHaveBeenCalled(); + expect(chainCredentials).toHaveBeenCalled(); + }); + }); + it("should allow assume roles assuming roles assuming roles ad infinitum", async () => { const roleArnOf = (id: string) => `arn:aws:iam::123456789:role/${id}`; const idOf = (roleArn: string) => roleArn.split("/")?.[1] ?? "UNKNOWN"; @@ -176,7 +254,7 @@ describe("fromTemporaryCredentials", () => { const SerialNumber = "SERIAL_NUMBER"; const mfaCode = "MFA_CODE"; const mfaCodeProvider = vi.fn().mockResolvedValue(mfaCode); - const provider = fromTemporaryCredentials({ + const provider = fromTemporaryCredentialsNode({ params: { RoleArn, SerialNumber, RoleSessionName }, mfaCodeProvider, }); @@ -197,7 +275,7 @@ describe("fromTemporaryCredentials", () => { it("should reject the promise with a terminal error if a MFA serial presents but mfaCodeProvider is missing", async () => { const SerialNumber = "SERIAL_NUMBER"; try { - await fromTemporaryCredentials({ + await fromTemporaryCredentialsNode({ params: { RoleArn, SerialNumber, RoleSessionName }, })(); fail("this test must fail"); diff --git a/packages/credential-providers/src/fromTemporaryCredentials.ts b/packages/credential-providers/src/fromTemporaryCredentials.ts index 6ffe7e57a774..679e0c66b8e4 100644 --- a/packages/credential-providers/src/fromTemporaryCredentials.ts +++ b/packages/credential-providers/src/fromTemporaryCredentials.ts @@ -1,15 +1,13 @@ -import type { AssumeRoleCommandInput, STSClient, STSClientConfig } from "@aws-sdk/nested-clients/sts"; -import type { CredentialProviderOptions } from "@aws-sdk/types"; -import { CredentialsProviderError } from "@smithy/property-provider"; -import { AwsCredentialIdentity, AwsCredentialIdentityProvider, Pluggable } from "@smithy/types"; +import type { RuntimeConfigAwsCredentialIdentityProvider } from "@aws-sdk/types"; -export interface FromTemporaryCredentialsOptions extends CredentialProviderOptions { - params: Omit & { RoleSessionName?: string }; - masterCredentials?: AwsCredentialIdentity | AwsCredentialIdentityProvider; - clientConfig?: STSClientConfig; - clientPlugins?: Pluggable[]; - mfaCodeProvider?: (mfaSerial: string) => Promise; -} +import { fromNodeProviderChain } from "./fromNodeProviderChain"; +import type { FromTemporaryCredentialsOptions } from "./fromTemporaryCredentials.browser"; +import { fromTemporaryCredentials as fromTemporaryCredentialsBase } from "./fromTemporaryCredentials.browser"; + +/** + * @public + */ +export { FromTemporaryCredentialsOptions }; /** * Creates a credential provider function that retrieves temporary credentials from STS AssumeRole API. @@ -53,45 +51,8 @@ export interface FromTemporaryCredentialsOptions extends CredentialProviderOptio * * @public */ -export const fromTemporaryCredentials = (options: FromTemporaryCredentialsOptions): AwsCredentialIdentityProvider => { - let stsClient: STSClient; - return async (): Promise => { - options.logger?.debug("@aws-sdk/credential-providers - fromTemporaryCredentials (STS)"); - const params = { ...options.params, RoleSessionName: options.params.RoleSessionName ?? "aws-sdk-js-" + Date.now() }; - if (params?.SerialNumber) { - if (!options.mfaCodeProvider) { - throw new CredentialsProviderError( - `Temporary credential requires multi-factor authentication,` + ` but no MFA code callback was provided.`, - { - tryNextLink: false, - logger: options.logger, - } - ); - } - params.TokenCode = await options.mfaCodeProvider(params?.SerialNumber); - } - - const { AssumeRoleCommand, STSClient } = await import("./loadSts"); - - if (!stsClient) stsClient = new STSClient({ ...options.clientConfig, credentials: options.masterCredentials }); - if (options.clientPlugins) { - for (const plugin of options.clientPlugins) { - stsClient.middlewareStack.use(plugin); - } - } - const { Credentials } = await stsClient.send(new AssumeRoleCommand(params)); - if (!Credentials || !Credentials.AccessKeyId || !Credentials.SecretAccessKey) { - throw new CredentialsProviderError(`Invalid response from STS.assumeRole call with role ${params.RoleArn}`, { - logger: options.logger, - }); - } - return { - accessKeyId: Credentials.AccessKeyId, - secretAccessKey: Credentials.SecretAccessKey, - sessionToken: Credentials.SessionToken, - expiration: Credentials.Expiration, - // TODO(credentialScope): access normally when shape is updated. - credentialScope: (Credentials as any).CredentialScope, - }; - }; +export const fromTemporaryCredentials = ( + options: FromTemporaryCredentialsOptions +): RuntimeConfigAwsCredentialIdentityProvider => { + return fromTemporaryCredentialsBase(options, fromNodeProviderChain); }; diff --git a/packages/credential-providers/src/index.browser.ts b/packages/credential-providers/src/index.browser.ts index 07d0adff00fb..bbde84908249 100644 --- a/packages/credential-providers/src/index.browser.ts +++ b/packages/credential-providers/src/index.browser.ts @@ -2,5 +2,5 @@ export * from "./fromCognitoIdentity"; export * from "./fromCognitoIdentityPool"; export { fromHttp } from "@aws-sdk/credential-provider-http"; export type { FromHttpOptions, HttpProviderCredentials } from "@aws-sdk/credential-provider-http"; -export * from "./fromTemporaryCredentials"; +export * from "./fromTemporaryCredentials.browser"; export * from "./fromWebToken"; diff --git a/packages/nested-clients/package.json b/packages/nested-clients/package.json index 96164d5d744f..510dd2da5d6d 100644 --- a/packages/nested-clients/package.json +++ b/packages/nested-clients/package.json @@ -86,8 +86,8 @@ "dist-*/**" ], "browser": { - "./dist-es/nested-sso-oidc/runtimeConfig": "./dist-es/nested-sso-oidc/runtimeConfig.browser", - "./dist-es/nested-sts/runtimeConfig": "./dist-es/nested-sts/runtimeConfig.browser" + "./dist-es/submodules/sts/runtimeConfig": "./dist-es/submodules/sts/runtimeConfig.browser", + "./dist-es/submodules/sso-oidc/runtimeConfig": "./dist-es/submodules/sso-oidc/runtimeConfig.browser" }, "react-native": {}, "homepage": "https://github.com/aws/aws-sdk-js-v3/tree/main/packages/nested-clients", diff --git a/packages/types/src/identity/AwsCredentialIdentity.ts b/packages/types/src/identity/AwsCredentialIdentity.ts index 903050883f91..1e7e87fccdda 100644 --- a/packages/types/src/identity/AwsCredentialIdentity.ts +++ b/packages/types/src/identity/AwsCredentialIdentity.ts @@ -1,4 +1,4 @@ -import type { AwsCredentialIdentity } from "@smithy/types"; +import type { AwsCredentialIdentity, AwsCredentialIdentityProvider } from "@smithy/types"; import type { AwsSdkCredentialsFeatures } from "../feature-ids"; @@ -11,6 +11,11 @@ export interface AwsIdentityProperties { callerClientConfig?: { region(): Promise; profile?: string; + /** + * @internal + * @deprecated + */ + credentialDefaultProvider?: (input?: any) => AwsCredentialIdentityProvider; }; }