From 72bf56d3ce301acc020b62dd1c3ccf8f926d8971 Mon Sep 17 00:00:00 2001 From: George Fu Date: Fri, 24 Oct 2025 14:23:58 -0400 Subject: [PATCH 1/7] fix(credential-providers): use modified region resolver for inner STS clients --- .../client-sts/src/defaultStsRoleAssumers.ts | 27 +- .../test/defaultRoleAssumers.spec.ts | 2 +- .../sts-client-defaultRoleAssumers.spec.ts | 2 +- .../sts-client-defaultStsRoleAssumers.ts | 27 +- .../src/fromIni.integ.spec.ts | 316 +++++++++++++++--- .../src/resolveSsoCredentials.spec.ts | 2 +- .../credential-provider-node.integ.spec.ts | 133 ++++++++ packages/credential-providers/README.md | 132 ++++++-- .../submodules/sts/defaultStsRoleAssumers.ts | 27 +- packages/region-config-resolver/package.json | 7 +- packages/region-config-resolver/src/index.ts | 1 + .../stsRegionDefaultResolver.browser.ts | 6 + .../stsRegionDefaultResolver.native.ts | 6 + .../stsRegionDefaultResolver.spec.ts | 27 ++ .../regionConfig/stsRegionDefaultResolver.ts | 20 ++ packages/types/src/credentials.ts | 1 + yarn.lock | 1 + 17 files changed, 633 insertions(+), 104 deletions(-) create mode 100644 packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.browser.ts create mode 100644 packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.native.ts create mode 100644 packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.spec.ts create mode 100644 packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.ts diff --git a/clients/client-sts/src/defaultStsRoleAssumers.ts b/clients/client-sts/src/defaultStsRoleAssumers.ts index bb7e08de7d18b..84970eaf6a246 100644 --- a/clients/client-sts/src/defaultStsRoleAssumers.ts +++ b/clients/client-sts/src/defaultStsRoleAssumers.ts @@ -2,6 +2,7 @@ // Please do not touch this file. It's generated from template in: // https://github.com/aws/aws-sdk-js-v3/blob/main/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultStsRoleAssumers.ts import { setCredentialFeature } from "@aws-sdk/core/client"; +import { stsRegionDefaultResolver } from "@aws-sdk/region-config-resolver"; import type { CredentialProviderOptions } from "@aws-sdk/types"; import { AwsCredentialIdentity, Logger, Provider } from "@smithy/types"; @@ -28,8 +29,6 @@ export type RoleAssumer = ( params: AssumeRoleCommandInput ) => Promise; -const ASSUME_ROLE_DEFAULT_REGION = "us-east-1"; - interface AssumedRoleUser { /** * The ARN of the temporary security credentials that are returned from the AssumeRole action. @@ -63,19 +62,21 @@ const getAccountIdFromAssumedRoleUser = (assumedRoleUser?: AssumedRoleUser) => { const resolveRegion = async ( _region: string | Provider | undefined, _parentRegion: string | Provider | undefined, - credentialProviderLogger?: Logger + credentialProviderLogger?: Logger, + loaderConfig: Parameters[0] = {} ): Promise => { const region: string | undefined = typeof _region === "function" ? await _region() : _region; const parentRegion: string | undefined = typeof _parentRegion === "function" ? await _parentRegion() : _parentRegion; + const stsDefaultRegion = await stsRegionDefaultResolver(loaderConfig)(); credentialProviderLogger?.debug?.( "@aws-sdk/client-sts::resolveRegion", "accepting first of:", - `${region} (provider)`, - `${parentRegion} (parent client)`, - `${ASSUME_ROLE_DEFAULT_REGION} (STS default)` + `${region} (credential provider clientConfig)`, + `${parentRegion} (contextual client)`, + `${stsDefaultRegion} (STS default: AWS_REGION, profile region, or us-east-1)` ); - return region ?? parentRegion ?? ASSUME_ROLE_DEFAULT_REGION; + return region ?? parentRegion ?? stsDefaultRegion; }; /** @@ -101,7 +102,11 @@ export const getDefaultRoleAssumer = ( const resolvedRegion = await resolveRegion( region, stsOptions?.parentClientConfig?.region, - credentialProviderLogger + credentialProviderLogger, + { + logger, + profile, + } ); const isCompatibleRequestHandler = !isH2(requestHandler); @@ -164,7 +169,11 @@ export const getDefaultRoleAssumerWithWebIdentity = ( const resolvedRegion = await resolveRegion( region, stsOptions?.parentClientConfig?.region, - credentialProviderLogger + credentialProviderLogger, + { + logger, + profile, + } ); const isCompatibleRequestHandler = !isH2(requestHandler); diff --git a/clients/client-sts/test/defaultRoleAssumers.spec.ts b/clients/client-sts/test/defaultRoleAssumers.spec.ts index 9daea2dc23fcc..9a647cd573806 100644 --- a/clients/client-sts/test/defaultRoleAssumers.spec.ts +++ b/clients/client-sts/test/defaultRoleAssumers.spec.ts @@ -139,7 +139,7 @@ describe("getDefaultRoleAssumer", () => { requestHandler: handler, parentClientConfig: { region: "some-other-region", - logger: null, + logger: null as any, requestHandler: null, }, }); diff --git a/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultRoleAssumers.spec.ts b/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultRoleAssumers.spec.ts index c3a96fec9bb90..3bebd5c381f6b 100644 --- a/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultRoleAssumers.spec.ts +++ b/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultRoleAssumers.spec.ts @@ -137,7 +137,7 @@ describe("getDefaultRoleAssumer", () => { requestHandler: handler, parentClientConfig: { region: "some-other-region", - logger: null, + logger: null as any, requestHandler: null, }, }); diff --git a/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultStsRoleAssumers.ts b/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultStsRoleAssumers.ts index 69a2d0019161e..d8edd0789e34e 100644 --- a/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultStsRoleAssumers.ts +++ b/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultStsRoleAssumers.ts @@ -1,4 +1,5 @@ import { setCredentialFeature } from "@aws-sdk/core/client"; +import { stsRegionDefaultResolver } from "@aws-sdk/region-config-resolver"; import type { CredentialProviderOptions } from "@aws-sdk/types"; import { AwsCredentialIdentity, Logger, Provider } from "@smithy/types"; @@ -25,8 +26,6 @@ export type RoleAssumer = ( params: AssumeRoleCommandInput ) => Promise; -const ASSUME_ROLE_DEFAULT_REGION = "us-east-1"; - interface AssumedRoleUser { /** * The ARN of the temporary security credentials that are returned from the AssumeRole action. @@ -60,19 +59,21 @@ const getAccountIdFromAssumedRoleUser = (assumedRoleUser?: AssumedRoleUser) => { const resolveRegion = async ( _region: string | Provider | undefined, _parentRegion: string | Provider | undefined, - credentialProviderLogger?: Logger + credentialProviderLogger?: Logger, + loaderConfig: Parameters[0] = {} ): Promise => { const region: string | undefined = typeof _region === "function" ? await _region() : _region; const parentRegion: string | undefined = typeof _parentRegion === "function" ? await _parentRegion() : _parentRegion; + const stsDefaultRegion = await stsRegionDefaultResolver(loaderConfig)(); credentialProviderLogger?.debug?.( "@aws-sdk/client-sts::resolveRegion", "accepting first of:", - `${region} (provider)`, - `${parentRegion} (parent client)`, - `${ASSUME_ROLE_DEFAULT_REGION} (STS default)` + `${region} (credential provider clientConfig)`, + `${parentRegion} (contextual client)`, + `${stsDefaultRegion} (STS default: AWS_REGION, profile region, or us-east-1)` ); - return region ?? parentRegion ?? ASSUME_ROLE_DEFAULT_REGION; + return region ?? parentRegion ?? stsDefaultRegion; }; /** @@ -98,7 +99,11 @@ export const getDefaultRoleAssumer = ( const resolvedRegion = await resolveRegion( region, stsOptions?.parentClientConfig?.region, - credentialProviderLogger + credentialProviderLogger, + { + logger, + profile, + } ); const isCompatibleRequestHandler = !isH2(requestHandler); @@ -161,7 +166,11 @@ export const getDefaultRoleAssumerWithWebIdentity = ( const resolvedRegion = await resolveRegion( region, stsOptions?.parentClientConfig?.region, - credentialProviderLogger + credentialProviderLogger, + { + logger, + profile, + } ); const isCompatibleRequestHandler = !isH2(requestHandler); diff --git a/packages/credential-provider-ini/src/fromIni.integ.spec.ts b/packages/credential-provider-ini/src/fromIni.integ.spec.ts index db87a4ad58229..0fbb2b66c25f9 100644 --- a/packages/credential-provider-ini/src/fromIni.integ.spec.ts +++ b/packages/credential-provider-ini/src/fromIni.integ.spec.ts @@ -1,43 +1,33 @@ import { STS } from "@aws-sdk/client-sts"; import { HttpRequest, HttpResponse } from "@smithy/protocol-http"; -import { SourceProfileInit } from "@smithy/shared-ini-file-loader"; +import { externalDataInterceptor } from "@smithy/shared-ini-file-loader"; import type { NodeHttpHandlerOptions, ParsedIniData } from "@smithy/types"; +import { homedir } from "node:os"; +import { join } from "node:path"; import { PassThrough } from "node:stream"; -import { afterEach, beforeEach, describe, expect, test as it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, test as it } from "vitest"; import { fromIni } from "./fromIni"; let iniProfileData: ParsedIniData = null as any; -vi.mock("@smithy/shared-ini-file-loader", async () => { - const actual: any = await vi.importActual("@smithy/shared-ini-file-loader"); - const pkg = { - ...actual, - async loadSsoSessionData() { - return Object.entries(iniProfileData) - .filter(([key]) => key.startsWith("sso-session.")) - .reduce( - (acc, [key, value]) => ({ - ...acc, - [key.split("sso-session.")[1]]: value, - }), - {} - ); - }, - async parseKnownFiles(init: SourceProfileInit): Promise { - return iniProfileData; - }, - async getSSOTokenFromFile() { - return { - accessToken: "mock_sso_token", - expiresAt: "3000-01-01T00:00:00.000Z", - }; - }, - }; - return { - ...pkg, - default: pkg, - }; -}); + +function setIniProfileData(data: ParsedIniData) { + iniProfileData = data; + let buffer = ""; + for (const profile in data) { + if (profile.startsWith("sso-session.")) { + buffer += `[sso-session ${profile.split("sso-session.")[1]}]\n`; + } else { + buffer += `[profile ${profile}]\n`; + } + for (const [k, v] of Object.entries(data[profile])) { + buffer += `${k} = ${v}\n`; + } + buffer += "\n"; + } + const dir = join(homedir(), ".aws"); + externalDataInterceptor.interceptFile(join(dir, "config"), buffer); +} class MockNodeHttpHandler { static create(instanceOrOptions?: any) { @@ -46,6 +36,7 @@ class MockNodeHttpHandler { } return new MockNodeHttpHandler(); } + async handle(request: HttpRequest) { const body = new PassThrough({}); @@ -125,7 +116,9 @@ class MockNodeHttpHandler { }), }; } + updateHttpClientConfig(key: keyof NodeHttpHandlerOptions, value: NodeHttpHandlerOptions[typeof key]): void {} + httpHandlerConfigs(): NodeHttpHandlerOptions { return null as any; } @@ -136,22 +129,20 @@ describe("fromIni region search order", () => { process.env.AWS_PROFILE = "default"; iniProfileData = { default: { - region: "us-west-2", output: "json", + region: "us-stsar-1", + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + external_id: "EXTERNAL_ID", + source_profile: "assume", + }, + assume: { + region: "us-stsar-1", + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", }, }; - iniProfileData.assume = { - region: "us-stsar-1", - aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", - aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", - }; - Object.assign(iniProfileData.default, { - region: "us-stsar-1", - role_arn: "ROLE_ARN", - role_session_name: "ROLE_SESSION_NAME", - external_id: "EXTERNAL_ID", - source_profile: "assume", - }); + setIniProfileData(iniProfileData); }); afterEach(() => { @@ -201,6 +192,7 @@ describe("fromIni region search order", () => { it("should use 3rd priority for the caller client", async () => { delete iniProfileData.default.region; + setIniProfileData(iniProfileData); const sts = new STS({ requestHandler: new MockNodeHttpHandler(), @@ -221,8 +213,78 @@ describe("fromIni region search order", () => { }); }); - it("should use 4th priority for the default partition's default region", async () => { - delete iniProfileData.default.region; + it("should use 4th priority for the config file region", async () => { + const credentialsData = await fromIni({ + clientConfig: { + requestHandler: new MockNodeHttpHandler(), + }, + })(); + + const sts = new STS({ + requestHandler: new MockNodeHttpHandler(), + credentials: credentialsData, + }); + + await sts.getCallerIdentity({}); + const credentials = await sts.config.credentials(); + expect(credentials).toMatchObject({ + accessKeyId: "STS_AR_ACCESS_KEY_ID", + secretAccessKey: "STS_AR_SECRET_ACCESS_KEY", + sessionToken: "STS_AR_SESSION_TOKEN_us-stsar-1", + }); + }); + + it("should use 5th priority for the AWS_REGION value", async () => { + process.env.AWS_REGION = "ap-northeast-1"; + iniProfileData = { + default: { + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + external_id: "EXTERNAL_ID", + source_profile: "assume", + }, + assume: { + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + }; + setIniProfileData(iniProfileData); + + const credentialsData = await fromIni({ + clientConfig: { + requestHandler: new MockNodeHttpHandler(), + }, + })(); + + const sts = new STS({ + requestHandler: new MockNodeHttpHandler(), + credentials: credentialsData, + }); + + await sts.getCallerIdentity({}); + const credentials = await sts.config.credentials(); + expect(credentials).toMatchObject({ + accessKeyId: "STS_AR_ACCESS_KEY_ID", + secretAccessKey: "STS_AR_SECRET_ACCESS_KEY", + sessionToken: "STS_AR_SESSION_TOKEN_ap-northeast-1", + }); + }); + + it("should use 6th priority for the default partition's default region", async () => { + delete process.env.AWS_REGION; + iniProfileData = { + default: { + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + external_id: "EXTERNAL_ID", + source_profile: "assume", + }, + assume: { + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + }; + setIniProfileData(iniProfileData); const credentialsData = await fromIni({ clientConfig: { @@ -244,4 +306,164 @@ describe("fromIni region search order", () => { sessionToken: "STS_AR_SESSION_TOKEN_us-east-1", }); }); + + describe("logic table", () => { + type Parameters = { + // has caller context client + withCaller: boolean; + // has region specified on the caller client + codeRegion: boolean; + // AWS_REGION is set + envRegion: boolean; + // profile regions are set + profileRegion: boolean; + // provider itself has a clientConfig.region + providerRegion: boolean; + // profile name + profile: string | undefined; + }; + + for (const withCaller of [true, false]) { + for (const codeRegion of [true, false]) { + for (const envRegion of [true, false]) { + for (const profileRegion of [true, false]) { + for (const providerRegion of [true, false]) { + for (const profile of ["default", "alt", undefined]) { + if (!codeRegion && !profileRegion && !envRegion) { + continue; + } + + const params = { + withCaller, + codeRegion, + envRegion, + profileRegion, + providerRegion, + profile, + }; + + it(`should resolve region as expected with params=${JSON.stringify(params)}`, async () => { + const region = await resolveStsRegion(params); + + if (providerRegion) { + expect(region).toBe("provider-region"); + return; + } + + if (profileRegion) { + expect(region).toBe(`${profile ?? "default"}-profile-region`); + return; + } + + if (codeRegion && withCaller) { + expect(region).toBe("code-region"); + return; + } + + if (envRegion) { + expect(region).toBe("env-region"); + return; + } + + expect(region).toBe("us-east-1"); + }); + } + } + } + } + } + } + + async function resolveStsRegion({ + withCaller, + envRegion, + profile, + profileRegion, + codeRegion, + providerRegion, + }: Parameters) { + if (envRegion) { + process.env.AWS_REGION = "env-region"; + } else { + delete process.env.AWS_REGION; + } + + if (profileRegion) { + iniProfileData = { + default: { + region: "default-profile-region", + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + external_id: "EXTERNAL_ID", + source_profile: "assume", + }, + assume: { + region: "assume-profile-region", + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + alt: { + region: "alt-profile-region", + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + external_id: "EXTERNAL_ID", + source_profile: "assume2", + }, + assume2: { + region: "assume2-profile-region", + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + }; + } else { + iniProfileData = { + default: { + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + external_id: "EXTERNAL_ID", + source_profile: "assume", + }, + assume: { + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + alt: { + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + external_id: "EXTERNAL_ID", + source_profile: "assume2", + }, + assume2: { + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + }; + } + setIniProfileData(iniProfileData); + + const provider = fromIni({ + profile, + clientConfig: { + region: providerRegion ? "provider-region" : undefined, + requestHandler: new MockNodeHttpHandler(), + }, + }); + + if (withCaller) { + const sts = new STS({ + profile, + requestHandler: new MockNodeHttpHandler(), + region: codeRegion ? "code-region" : undefined, + credentials: provider, + }); + + await sts.getCallerIdentity({}); + const credentials = await sts.config.credentials(); + return credentials.sessionToken!.replace("STS_AR_SESSION_TOKEN_", ""); + } + + const credentials = await provider(); + return credentials.sessionToken!.replace("STS_AR_SESSION_TOKEN_", ""); + } + }); }); diff --git a/packages/credential-provider-ini/src/resolveSsoCredentials.spec.ts b/packages/credential-provider-ini/src/resolveSsoCredentials.spec.ts index e9fb9f38475ff..feeb2a5dda928 100644 --- a/packages/credential-provider-ini/src/resolveSsoCredentials.spec.ts +++ b/packages/credential-provider-ini/src/resolveSsoCredentials.spec.ts @@ -64,7 +64,7 @@ describe(resolveSsoCredentials.name, () => { secretAccessKey: "mockSecretAccessKey", }; const requestHandler = vi.fn(); - const logger = vi.fn(); + const logger: any = vi.fn(); vi.mocked(fromSSO).mockReturnValue(() => Promise.resolve(mockCreds)); diff --git a/packages/credential-provider-node/tests/credential-provider-node.integ.spec.ts b/packages/credential-provider-node/tests/credential-provider-node.integ.spec.ts index b9a9412097bbe..6c6970e7c8ce5 100644 --- a/packages/credential-provider-node/tests/credential-provider-node.integ.spec.ts +++ b/packages/credential-provider-node/tests/credential-provider-node.integ.spec.ts @@ -805,6 +805,24 @@ describe("credential-provider-node integration test", () => { }); }); + it("prefers AWS_REGION over profile region when profile is not the credential source", async () => { + process.env.AWS_REGION = "eu-north-1"; + const provider = fromTokenFile({ + roleArn: "ROLE_ARN", + webIdentityTokenFile: "token-filepath", + }); + const credentials = await provider(); + expect(credentials).toEqual({ + accessKeyId: "STS_ARWI_ACCESS_KEY_ID", + secretAccessKey: "STS_ARWI_SECRET_ACCESS_KEY", + sessionToken: "STS_ARWI_SESSION_TOKEN_eu-north-1", + expiration: new Date("3000-01-01T00:00:00.000Z"), + $source: { + CREDENTIALS_STS_ASSUME_ROLE_WEB_ID: "k", + }, + }); + }); + it("should use the caller client region if derived from AWS_REGION", async () => { process.env.AWS_REGION = "eu-west-2"; const provider = fromTokenFile({ @@ -1492,5 +1510,120 @@ describe("credential-provider-node integration test", () => { expect(logger.debug).toHaveBeenCalled(); expect(logger.info).toHaveBeenCalled(); }); + + describe("uses a variant of the default region resolution where us-east-1 is the last resort", async () => { + it("no env or profile region", async () => { + delete process.env.AWS_REGION; + setIniProfileData({ + assume: { + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + default: { + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + source_profile: "assume", + }, + }); + + const provider = defaultProvider({}); + const credentials = await provider(); + expect(credentials).toEqual({ + accessKeyId: "STS_AR_ACCESS_KEY_ID", + expiration: new Date(`3000-01-01T00:00:00.000Z`), + secretAccessKey: "STS_AR_SECRET_ACCESS_KEY", + sessionToken: "STS_AR_SESSION_TOKEN_us-east-1", + $source: { + CREDENTIALS_PROFILE_SOURCE_PROFILE: "o", + CREDENTIALS_STS_ASSUME_ROLE: "i", + }, + }); + }); + + it("env region", async () => { + process.env.AWS_REGION = "eu-north-1"; + setIniProfileData({ + assume: { + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + default: { + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + source_profile: "assume", + }, + }); + + const provider = defaultProvider({}); + const credentials = await provider(); + expect(credentials).toEqual({ + accessKeyId: "STS_AR_ACCESS_KEY_ID", + expiration: new Date(`3000-01-01T00:00:00.000Z`), + secretAccessKey: "STS_AR_SECRET_ACCESS_KEY", + sessionToken: "STS_AR_SESSION_TOKEN_eu-north-1", + $source: { + CREDENTIALS_PROFILE_SOURCE_PROFILE: "o", + CREDENTIALS_STS_ASSUME_ROLE: "i", + }, + }); + }); + + it("profile region", async () => { + setIniProfileData({ + assume: { + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + default: { + region: "us-west-2", + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + source_profile: "assume", + }, + }); + + const provider = defaultProvider({}); + const credentials = await provider(); + expect(credentials).toEqual({ + accessKeyId: "STS_AR_ACCESS_KEY_ID", + expiration: new Date(`3000-01-01T00:00:00.000Z`), + secretAccessKey: "STS_AR_SECRET_ACCESS_KEY", + sessionToken: "STS_AR_SESSION_TOKEN_us-west-2", + $source: { + CREDENTIALS_PROFILE_SOURCE_PROFILE: "o", + CREDENTIALS_STS_ASSUME_ROLE: "i", + }, + }); + }); + + it("profile and env region conflict, it uses config region because credentials are fromIni", async () => { + process.env.AWS_REGION = "eu-north-1"; + setIniProfileData({ + assume: { + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + default: { + region: "us-west-2", + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + source_profile: "assume", + }, + }); + + const provider = defaultProvider({}); + const credentials = await provider(); + expect(credentials).toEqual({ + accessKeyId: "STS_AR_ACCESS_KEY_ID", + expiration: new Date(`3000-01-01T00:00:00.000Z`), + secretAccessKey: "STS_AR_SECRET_ACCESS_KEY", + sessionToken: "STS_AR_SESSION_TOKEN_us-west-2", + $source: { + CREDENTIALS_PROFILE_SOURCE_PROFILE: "o", + CREDENTIALS_STS_ASSUME_ROLE: "i", + }, + }); + }); + }); }); }); diff --git a/packages/credential-providers/README.md b/packages/credential-providers/README.md index 4b04e81be3c51..c9faebdf3812a 100644 --- a/packages/credential-providers/README.md +++ b/packages/credential-providers/README.md @@ -7,26 +7,29 @@ A collection of all credential providers. # Table of Contents -1. [Terminology](#terminology) -1. [From Cognito Identity](#fromcognitoidentity) -1. [From Cognito Identity Pool](#fromcognitoidentitypool) -1. [From Temporary Credentials](#fromtemporarycredentials) -1. [From Web Token](#fromwebtoken) - 1. [Examples](#examples) -1. [From Token File](#fromtokenfile) -1. [From Instance and Container Metadata Service](#fromcontainermetadata-and-frominstancemetadata) -1. [From HTTP(S)](#fromhttp) -1. [From Shared INI files](#fromini) - 1. [Sample Files](#sample-files) -1. [From Environmental Variables](#fromenv) -1. [From Credential Process](#fromprocess) - 1. [Sample files](#sample-files-1) -1. [From Single Sign-On Service](#fromsso) - 1. [Supported Configuration](#supported-configuration) - 1. [SSO login with AWS CLI](#sso-login-with-the-aws-cli) - 1. [Sample Files](#sample-files-2) -1. [From Node.js default credentials provider chain](#fromnodeproviderchain) -1. [Creating a custom credentials chain](#createcredentialchain) +- [Terminology](#terminology) + - [Credentials Provider](#credentials-provider) + - [Outer and inner clients](#outer-and-inner-clients) +- [Resolving AWS Region in credential providers (e.g. STS region)](#resolving-aws-region-in-credential-providers-eg-sts-region) +- [From Cognito Identity](#fromcognitoidentity) +- [From Cognito Identity Pool](#fromcognitoidentitypool) +- [From Temporary Credentials](#fromtemporarycredentials) +- [From Web Token](#fromwebtoken) + - [Examples](#examples) +- [From Token File](#fromtokenfile) +- [From Instance and Container Metadata Service](#fromcontainermetadata-and-frominstancemetadata) +- [From HTTP(S)](#fromhttp) +- [From Shared INI files](#fromini) + - [Sample Files](#sample-files) +- [From Environmental Variables](#fromenv) +- [From Credential Process](#fromprocess) + - [Sample files](#sample-files-1) +- [From Single Sign-On Service](#fromsso) + - [Supported Configuration](#supported-configuration) + - [SSO login with AWS CLI](#sso-login-with-the-aws-cli) + - [Sample Files](#sample-files-2) +- [From Node.js default credentials provider chain](#fromnodeproviderchain) +- [Creating a custom credentials chain](#createcredentialchain) ## Terminology @@ -80,6 +83,77 @@ In the above example, `S3Client` is the outer client, and if the `fromIni` credentials provider uses STS::AssumeRole, the `STSClient` initialized by the SDK is the inner client. +## Resolving AWS Region in credential providers (e.g. STS region) + +When a credential provider uses an SDK client to retrieve credentials, commonly STS, the +control of the STS region follows this logic: + +```js +import { fromIni } from "@aws-sdk/credential-providers"; + +/* +# AWS config file contents +[profile default] +region = profile-region +role_arn = ROLE_ARN +source_profile = assume + +[profile assume] +... + */ + +process.env.AWS_REGION = "env-region"; + +const provider = fromIni({ + clientConfig: { + region: "credential-provider-config-region", + }, +}); + +const client = new SDKClient({ + region: "context-client-region", + credentials: provider, +}); + +const fallbackRegion = "us-east-1"; +``` + +As shown above, there are many sources of region information. The priority order is: + +1. `credential-provider-config-region` - given in code to the credential provider itself. +2. `profile-region` **\*** - if credential provider is resolving credentials from the config file, the config file's + region takes precedence in this case over AWS_REGION env. +3. `context-client-region` - the region resolved by an SDK client using the credential provider. +4. `env-region` - AWS_REGION environment variable. +5. `profile-region` **\*** - if credential provider is not resolving credentials from the config file, the config file's + region is lower priority than AWS_REGION env. +6. `us-east-1` (fallback) - this is a legacy fallback value. It's more likely that the client will fail to execute any + operation if none of the other region sources were set. + +This differs from _direct_ instantiation of the STSClient, which follows this order, which is the same for all clients: + +```js +import { STSClient } from "@aws-sdk/client-sts"; +/* +# AWS config file contents +[profile default] +region = profile-region + */ + +process.env.AWS_REGION = "env-region"; + +const client = new STSClient({ + region: "client-region", +}); +``` + +1. `client-region` +2. `env-region` +3. `profile-region` (config file) +4. thrown error (no us-east-1 fallback) + +# Credential providers + ## `fromCognitoIdentity()` - Uses `@aws-sdk/client-cognito-identity` @@ -898,20 +972,26 @@ const credentialProvider = fromNodeProviderChain({ You can use this helper to create a credential chain of your own. -A credential chain is created from a list of functions of the signature () => Promise<[AwsCredentialIdentity](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-smithy-types/Interface/AwsCredentialIdentity/)>, +A credential chain is created from a list of functions of the signature () => +Promise<[AwsCredentialIdentity](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-smithy-types/Interface/AwsCredentialIdentity/)>, composed together such that the overall chain has the **same** signature. -That is why you can provide the chained credential provider to the same field (`credentials`) as any single provider function. +That is why you can provide the chained credential provider to the same field (`credentials`) as any single provider +function. All the providers from this package are compatible, and can be used to create such a chain. -As with _any_ function provided to the `credentials` SDK client constructor configuration field, if the credential object returned does not contain -an `expiration` (type `Date`), the client will only ever call the provider function once. You do not need to memoize this function. +As with _any_ function provided to the `credentials` SDK client constructor configuration field, if the credential +object returned does not contain +an `expiration` (type `Date`), the client will only ever call the provider function once. You do not need to memoize +this function. -To enable automatic refresh, the credential provider function should set an `expiration` (`Date`) field. When this expiration approaches within 5 minutes, the +To enable automatic refresh, the credential provider function should set an `expiration` (`Date`) field. When this +expiration approaches within 5 minutes, the provider function will be called again by the client in the course of making SDK requests. -To assist with this, the `createCredentialChain` has a chainable helper `.expireAfter(milliseconds: number)`. An example is included below. +To assist with this, the `createCredentialChain` has a chainable helper `.expireAfter(milliseconds: number)`. An example +is included below. ```ts import { fromEnv, fromIni, createCredentialChain } from "@aws-sdk/credential-providers"; diff --git a/packages/nested-clients/src/submodules/sts/defaultStsRoleAssumers.ts b/packages/nested-clients/src/submodules/sts/defaultStsRoleAssumers.ts index bb7e08de7d18b..84970eaf6a246 100644 --- a/packages/nested-clients/src/submodules/sts/defaultStsRoleAssumers.ts +++ b/packages/nested-clients/src/submodules/sts/defaultStsRoleAssumers.ts @@ -2,6 +2,7 @@ // Please do not touch this file. It's generated from template in: // https://github.com/aws/aws-sdk-js-v3/blob/main/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultStsRoleAssumers.ts import { setCredentialFeature } from "@aws-sdk/core/client"; +import { stsRegionDefaultResolver } from "@aws-sdk/region-config-resolver"; import type { CredentialProviderOptions } from "@aws-sdk/types"; import { AwsCredentialIdentity, Logger, Provider } from "@smithy/types"; @@ -28,8 +29,6 @@ export type RoleAssumer = ( params: AssumeRoleCommandInput ) => Promise; -const ASSUME_ROLE_DEFAULT_REGION = "us-east-1"; - interface AssumedRoleUser { /** * The ARN of the temporary security credentials that are returned from the AssumeRole action. @@ -63,19 +62,21 @@ const getAccountIdFromAssumedRoleUser = (assumedRoleUser?: AssumedRoleUser) => { const resolveRegion = async ( _region: string | Provider | undefined, _parentRegion: string | Provider | undefined, - credentialProviderLogger?: Logger + credentialProviderLogger?: Logger, + loaderConfig: Parameters[0] = {} ): Promise => { const region: string | undefined = typeof _region === "function" ? await _region() : _region; const parentRegion: string | undefined = typeof _parentRegion === "function" ? await _parentRegion() : _parentRegion; + const stsDefaultRegion = await stsRegionDefaultResolver(loaderConfig)(); credentialProviderLogger?.debug?.( "@aws-sdk/client-sts::resolveRegion", "accepting first of:", - `${region} (provider)`, - `${parentRegion} (parent client)`, - `${ASSUME_ROLE_DEFAULT_REGION} (STS default)` + `${region} (credential provider clientConfig)`, + `${parentRegion} (contextual client)`, + `${stsDefaultRegion} (STS default: AWS_REGION, profile region, or us-east-1)` ); - return region ?? parentRegion ?? ASSUME_ROLE_DEFAULT_REGION; + return region ?? parentRegion ?? stsDefaultRegion; }; /** @@ -101,7 +102,11 @@ export const getDefaultRoleAssumer = ( const resolvedRegion = await resolveRegion( region, stsOptions?.parentClientConfig?.region, - credentialProviderLogger + credentialProviderLogger, + { + logger, + profile, + } ); const isCompatibleRequestHandler = !isH2(requestHandler); @@ -164,7 +169,11 @@ export const getDefaultRoleAssumerWithWebIdentity = ( const resolvedRegion = await resolveRegion( region, stsOptions?.parentClientConfig?.region, - credentialProviderLogger + credentialProviderLogger, + { + logger, + profile, + } ); const isCompatibleRequestHandler = !isH2(requestHandler); diff --git a/packages/region-config-resolver/package.json b/packages/region-config-resolver/package.json index c4000efdc6d61..1ab0fba1aafc7 100644 --- a/packages/region-config-resolver/package.json +++ b/packages/region-config-resolver/package.json @@ -25,6 +25,7 @@ "dependencies": { "@aws-sdk/types": "*", "@smithy/config-resolver": "^4.4.0", + "@smithy/node-config-provider": "^4.3.3", "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, @@ -53,5 +54,9 @@ "type": "git", "url": "https://github.com/aws/aws-sdk-js-v3.git", "directory": "packages/region-config-resolver" - } + }, + "browser": { + "./dist-es/regionConfig/stsRegionDefaultResolver": "./dist-es/regionConfig/stsRegionDefaultResolver.browser" + }, + "react-native": {} } diff --git a/packages/region-config-resolver/src/index.ts b/packages/region-config-resolver/src/index.ts index f42620e237cf5..d685b15dc9ee0 100644 --- a/packages/region-config-resolver/src/index.ts +++ b/packages/region-config-resolver/src/index.ts @@ -1,2 +1,3 @@ export * from "./extensions"; export * from "./regionConfig/awsRegionConfig"; +export * from "./regionConfig/stsRegionDefaultResolver"; diff --git a/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.browser.ts b/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.browser.ts new file mode 100644 index 0000000000000..8a03c78cccee2 --- /dev/null +++ b/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.browser.ts @@ -0,0 +1,6 @@ +/** + * @internal + */ +export function stsRegionDefaultResolver() { + return async () => "us-east-1"; +} diff --git a/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.native.ts b/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.native.ts new file mode 100644 index 0000000000000..8a03c78cccee2 --- /dev/null +++ b/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.native.ts @@ -0,0 +1,6 @@ +/** + * @internal + */ +export function stsRegionDefaultResolver() { + return async () => "us-east-1"; +} diff --git a/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.spec.ts b/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.spec.ts new file mode 100644 index 0000000000000..433d1c5339647 --- /dev/null +++ b/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.spec.ts @@ -0,0 +1,27 @@ +import { describe, expect, test as it } from "vitest"; + +import { stsRegionDefaultResolver } from "./stsRegionDefaultResolver"; +import { stsRegionDefaultResolver as browser } from "./stsRegionDefaultResolver.browser"; +import { stsRegionDefaultResolver as native } from "./stsRegionDefaultResolver.native"; + +describe("stsRegionDefaultResolver", () => { + for (const impl of [stsRegionDefaultResolver, native, browser]) { + it(`should default to us-east-1`, async () => { + delete process.env.AWS_REGION; + const regionProvider = impl({ + profile: "non-existent P R O F I L E", + }); + const region = await regionProvider(); + expect(region).toBe("us-east-1"); + }); + } + + it("should use AWS_REGION before fallback to us-east-1", async () => { + process.env.AWS_REGION = "us-west-2"; + const regionProvider = stsRegionDefaultResolver({ + profile: "non-existent P R O F I L E", + }); + const region = await regionProvider(); + expect(region).toBe("us-west-2"); + }); +}); diff --git a/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.ts b/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.ts new file mode 100644 index 0000000000000..c66fa8bf14fab --- /dev/null +++ b/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.ts @@ -0,0 +1,20 @@ +import { NODE_REGION_CONFIG_FILE_OPTIONS, NODE_REGION_CONFIG_OPTIONS } from "@smithy/config-resolver"; +import { type LocalConfigOptions, loadConfig } from "@smithy/node-config-provider"; + +/** + * Default region provider for STS when used as an inner client. + * Differs from the default region resolver in that us-east-1 is the fallback instead of throwing an error. + * + * @internal + */ +export function stsRegionDefaultResolver(loaderConfig: LocalConfigOptions = {}) { + return loadConfig( + { + ...NODE_REGION_CONFIG_OPTIONS, + async default() { + return "us-east-1"; + }, + }, + { ...NODE_REGION_CONFIG_FILE_OPTIONS, ...loaderConfig } + ); +} diff --git a/packages/types/src/credentials.ts b/packages/types/src/credentials.ts index e7abfbf8c3e21..a9c175ff32525 100644 --- a/packages/types/src/credentials.ts +++ b/packages/types/src/credentials.ts @@ -49,6 +49,7 @@ export type CredentialProviderOptions = { parentClientConfig?: { region?: string | Provider; profile?: string; + logger?: Logger; [key: string]: unknown; }; }; diff --git a/yarn.lock b/yarn.lock index 836e384b6cc0f..6d0a99288c283 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24034,6 +24034,7 @@ __metadata: dependencies: "@aws-sdk/types": "npm:*" "@smithy/config-resolver": "npm:^4.4.0" + "@smithy/node-config-provider": "npm:^4.3.3" "@smithy/types": "npm:^4.8.0" "@tsconfig/recommended": "npm:1.0.1" concurrently: "npm:7.0.0" From 5b9b13616133b4c5da7f6ad22428a210ab21d6c3 Mon Sep 17 00:00:00 2001 From: George Fu Date: Mon, 27 Oct 2025 15:56:05 -0400 Subject: [PATCH 2/7] chore: aws chain memoizer --- .../src/fromIni.integ.spec.ts | 160 -------------- .../src/defaultProvider.spec.ts | 4 +- .../src/defaultProvider.ts | 35 ++-- .../src/runtime/memoize-chain.spec.ts | 134 ++++++++++++ .../src/runtime/memoize-chain.ts | 74 +++++++ .../credential-provider-node.integ.spec.ts | 196 +++++++++++++++++- .../src/createCredentialChain.ts | 5 +- 7 files changed, 418 insertions(+), 190 deletions(-) create mode 100644 packages/credential-provider-node/src/runtime/memoize-chain.spec.ts create mode 100644 packages/credential-provider-node/src/runtime/memoize-chain.ts diff --git a/packages/credential-provider-ini/src/fromIni.integ.spec.ts b/packages/credential-provider-ini/src/fromIni.integ.spec.ts index 0fbb2b66c25f9..3609c83b91e4e 100644 --- a/packages/credential-provider-ini/src/fromIni.integ.spec.ts +++ b/packages/credential-provider-ini/src/fromIni.integ.spec.ts @@ -306,164 +306,4 @@ describe("fromIni region search order", () => { sessionToken: "STS_AR_SESSION_TOKEN_us-east-1", }); }); - - describe("logic table", () => { - type Parameters = { - // has caller context client - withCaller: boolean; - // has region specified on the caller client - codeRegion: boolean; - // AWS_REGION is set - envRegion: boolean; - // profile regions are set - profileRegion: boolean; - // provider itself has a clientConfig.region - providerRegion: boolean; - // profile name - profile: string | undefined; - }; - - for (const withCaller of [true, false]) { - for (const codeRegion of [true, false]) { - for (const envRegion of [true, false]) { - for (const profileRegion of [true, false]) { - for (const providerRegion of [true, false]) { - for (const profile of ["default", "alt", undefined]) { - if (!codeRegion && !profileRegion && !envRegion) { - continue; - } - - const params = { - withCaller, - codeRegion, - envRegion, - profileRegion, - providerRegion, - profile, - }; - - it(`should resolve region as expected with params=${JSON.stringify(params)}`, async () => { - const region = await resolveStsRegion(params); - - if (providerRegion) { - expect(region).toBe("provider-region"); - return; - } - - if (profileRegion) { - expect(region).toBe(`${profile ?? "default"}-profile-region`); - return; - } - - if (codeRegion && withCaller) { - expect(region).toBe("code-region"); - return; - } - - if (envRegion) { - expect(region).toBe("env-region"); - return; - } - - expect(region).toBe("us-east-1"); - }); - } - } - } - } - } - } - - async function resolveStsRegion({ - withCaller, - envRegion, - profile, - profileRegion, - codeRegion, - providerRegion, - }: Parameters) { - if (envRegion) { - process.env.AWS_REGION = "env-region"; - } else { - delete process.env.AWS_REGION; - } - - if (profileRegion) { - iniProfileData = { - default: { - region: "default-profile-region", - role_arn: "ROLE_ARN", - role_session_name: "ROLE_SESSION_NAME", - external_id: "EXTERNAL_ID", - source_profile: "assume", - }, - assume: { - region: "assume-profile-region", - aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", - aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", - }, - alt: { - region: "alt-profile-region", - role_arn: "ROLE_ARN", - role_session_name: "ROLE_SESSION_NAME", - external_id: "EXTERNAL_ID", - source_profile: "assume2", - }, - assume2: { - region: "assume2-profile-region", - aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", - aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", - }, - }; - } else { - iniProfileData = { - default: { - role_arn: "ROLE_ARN", - role_session_name: "ROLE_SESSION_NAME", - external_id: "EXTERNAL_ID", - source_profile: "assume", - }, - assume: { - aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", - aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", - }, - alt: { - role_arn: "ROLE_ARN", - role_session_name: "ROLE_SESSION_NAME", - external_id: "EXTERNAL_ID", - source_profile: "assume2", - }, - assume2: { - aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", - aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", - }, - }; - } - setIniProfileData(iniProfileData); - - const provider = fromIni({ - profile, - clientConfig: { - region: providerRegion ? "provider-region" : undefined, - requestHandler: new MockNodeHttpHandler(), - }, - }); - - if (withCaller) { - const sts = new STS({ - profile, - requestHandler: new MockNodeHttpHandler(), - region: codeRegion ? "code-region" : undefined, - credentials: provider, - }); - - await sts.getCallerIdentity({}); - const credentials = await sts.config.credentials(); - return credentials.sessionToken!.replace("STS_AR_SESSION_TOKEN_", ""); - } - - const credentials = await provider(); - return credentials.sessionToken!.replace("STS_AR_SESSION_TOKEN_", ""); - } - }); }); diff --git a/packages/credential-provider-node/src/defaultProvider.spec.ts b/packages/credential-provider-node/src/defaultProvider.spec.ts index c078787148b1c..024672d11c36e 100644 --- a/packages/credential-provider-node/src/defaultProvider.spec.ts +++ b/packages/credential-provider-node/src/defaultProvider.spec.ts @@ -26,7 +26,9 @@ describe(defaultProvider.name, () => { }; const credentials = () => { - throw new CredentialsProviderError("test", true); + throw new CredentialsProviderError("test", { + tryNextLink: true, + }); }; const finalCredentials = () => { diff --git a/packages/credential-provider-node/src/defaultProvider.ts b/packages/credential-provider-node/src/defaultProvider.ts index a629d5e27f65e..0e3e7ebcf03fd 100644 --- a/packages/credential-provider-node/src/defaultProvider.ts +++ b/packages/credential-provider-node/src/defaultProvider.ts @@ -4,12 +4,14 @@ import type { FromIniInit } from "@aws-sdk/credential-provider-ini"; import type { FromProcessInit } from "@aws-sdk/credential-provider-process"; import type { FromSSOInit, SsoCredentialsParameters } from "@aws-sdk/credential-provider-sso"; import type { FromTokenFileInit } from "@aws-sdk/credential-provider-web-identity"; +import type { AwsIdentityProperties } from "@aws-sdk/types"; import type { RemoteProviderInit } from "@smithy/credential-provider-imds"; -import { chain, CredentialsProviderError, memoize } from "@smithy/property-provider"; +import { CredentialsProviderError } from "@smithy/property-provider"; import { ENV_PROFILE } from "@smithy/shared-ini-file-loader"; -import { AwsCredentialIdentity, MemoizedProvider } from "@smithy/types"; +import type { AwsCredentialIdentity } from "@smithy/types"; import { remoteProvider } from "./remoteProvider"; +import { type MemoizedRuntimeConfigAwsCredentialIdentityProvider, memoizeChain } from "./runtime/memoize-chain"; /** * @public @@ -60,9 +62,9 @@ let multipleCredentialSourceWarningEmitted = false; * @see {@link fromContainerMetadata} The function used to source credentials from the * ECS Container Metadata Service. */ -export const defaultProvider = (init: DefaultProviderInit = {}): MemoizedProvider => - memoize( - chain( +export const defaultProvider = (init: DefaultProviderInit = {}): MemoizedRuntimeConfigAwsCredentialIdentityProvider => + memoizeChain( + [ async () => { const profile = init.profile ?? process.env[ENV_PROFILE]; if (profile) { @@ -95,7 +97,7 @@ export const defaultProvider = (init: DefaultProviderInit = {}): MemoizedProvide init.logger?.debug("@aws-sdk/credential-provider-node - defaultProvider::fromEnv"); return fromEnv(init)(); }, - async () => { + async (awsIdentityProperties?: AwsIdentityProperties) => { init.logger?.debug("@aws-sdk/credential-provider-node - defaultProvider::fromSSO"); const { ssoStartUrl, ssoAccountId, ssoRegion, ssoRoleName, ssoSession } = init; if (!ssoStartUrl && !ssoAccountId && !ssoRegion && !ssoRoleName && !ssoSession) { @@ -105,22 +107,22 @@ export const defaultProvider = (init: DefaultProviderInit = {}): MemoizedProvide ); } const { fromSSO } = await import("@aws-sdk/credential-provider-sso"); - return fromSSO(init)(); + return fromSSO(init)(awsIdentityProperties); }, - async () => { + async (awsIdentityProperties?: AwsIdentityProperties) => { init.logger?.debug("@aws-sdk/credential-provider-node - defaultProvider::fromIni"); const { fromIni } = await import("@aws-sdk/credential-provider-ini"); - return fromIni(init)(); + return fromIni(init)(awsIdentityProperties); }, - async () => { + async (awsIdentityProperties?: AwsIdentityProperties) => { init.logger?.debug("@aws-sdk/credential-provider-node - defaultProvider::fromProcess"); const { fromProcess } = await import("@aws-sdk/credential-provider-process"); - return fromProcess(init)(); + return fromProcess(init)(awsIdentityProperties); }, - async () => { + async (awsIdentityProperties?: AwsIdentityProperties) => { init.logger?.debug("@aws-sdk/credential-provider-node - defaultProvider::fromTokenFile"); const { fromTokenFile } = await import("@aws-sdk/credential-provider-web-identity"); - return fromTokenFile(init)(); + return fromTokenFile(init)(awsIdentityProperties); }, async () => { init.logger?.debug("@aws-sdk/credential-provider-node - defaultProvider::remoteProvider"); @@ -131,10 +133,9 @@ export const defaultProvider = (init: DefaultProviderInit = {}): MemoizedProvide tryNextLink: false, logger: init.logger, }); - } - ), - credentialsTreatedAsExpired, - credentialsWillNeedRefresh + }, + ], + credentialsTreatedAsExpired ); /** diff --git a/packages/credential-provider-node/src/runtime/memoize-chain.spec.ts b/packages/credential-provider-node/src/runtime/memoize-chain.spec.ts new file mode 100644 index 0000000000000..0817db9067148 --- /dev/null +++ b/packages/credential-provider-node/src/runtime/memoize-chain.spec.ts @@ -0,0 +1,134 @@ +import type { AwsIdentityProperties, RuntimeConfigAwsCredentialIdentityProvider } from "@aws-sdk/types"; +import { beforeEach, describe, expect, test as it, vi } from "vitest"; + +import { credentialsWillNeedRefresh } from "../defaultProvider"; +import { memoizeChain } from "./memoize-chain"; + +describe("memoize runtime config aware AWS credential chain", () => { + let staticCredentials!: RuntimeConfigAwsCredentialIdentityProvider; + let expiringCredentials!: RuntimeConfigAwsCredentialIdentityProvider; + + const expiration = new Date(); + + beforeEach(() => { + vi.resetAllMocks(); + staticCredentials = vi.fn().mockImplementation(async (options?: AwsIdentityProperties) => { + await new Promise((r) => setTimeout(r, 100)); + return { + accessKeyId: "", + secretAccessKey: "", + runtimeOptions: Object.keys(options ?? {}).concat(Object.keys(options?.callerClientConfig ?? {})), + }; + }); + + let sequence = 0; + + expiringCredentials = vi.fn().mockImplementation(async (options?: AwsIdentityProperties) => { + await new Promise((r) => setTimeout(r, 100)); + return { + accessKeyId: "", + secretAccessKey: "", + expiration, + sequence: sequence++, + runtimeOptions: Object.keys(options ?? {}).concat(Object.keys(options?.callerClientConfig ?? {})), + }; + }); + }); + + it("should call composed provider functions", async () => { + const provider = memoizeChain([staticCredentials], credentialsWillNeedRefresh); + + const credentials = await provider({ + callerClientConfig: { + region: async () => "context-region", + profile: "alt", + }, + }); + + expect(credentials).toEqual({ + accessKeyId: "", + secretAccessKey: "", + runtimeOptions: ["callerClientConfig", "region", "profile"], + }); + expect(staticCredentials).toHaveBeenCalledTimes(1); + }); + + it("should use an active lock when no credentials exist", async () => { + const provider = memoizeChain([staticCredentials], credentialsWillNeedRefresh); + + const [credentials] = await Promise.all([provider(), provider(), provider(), provider(), provider()]); + + expect(credentials).toEqual({ + accessKeyId: "", + secretAccessKey: "", + runtimeOptions: [], + }); + expect(staticCredentials).toHaveBeenCalledTimes(1); + }); + + it("should use a cache", async () => { + const provider = memoizeChain([staticCredentials], credentialsWillNeedRefresh); + + await Promise.all([provider(), provider(), provider(), provider(), provider()]); + const [credentials] = await Promise.all([provider(), provider(), provider(), provider(), provider()]); + + expect(credentials).toEqual({ + accessKeyId: "", + secretAccessKey: "", + runtimeOptions: [], + }); + expect(staticCredentials).toHaveBeenCalledTimes(1); + }); + + it("should use a passive lock when credentials do exist", async () => { + const provider = memoizeChain([expiringCredentials], credentialsWillNeedRefresh); + + { + // initial invocation returns sequence-0 credentials. + const credentials = await Promise.all([provider(), provider(), provider(), provider(), provider()]); + for (const c of credentials) { + expect(c).toEqual({ + accessKeyId: "", + secretAccessKey: "", + expiration, + sequence: 0, + runtimeOptions: [], + }); + } + expect(expiringCredentials).toHaveBeenCalledTimes(1); + } + + { + // second invocation returns sequence-0 credentials, but background initializes refresh. + const credentials = await Promise.all([provider(), provider(), provider(), provider(), provider()]); + for (const c of credentials) { + expect(c).toEqual({ + accessKeyId: "", + secretAccessKey: "", + expiration, + sequence: 0, + runtimeOptions: [], + }); + } + expect(expiringCredentials).toHaveBeenCalledTimes(2); + } + + // allow new credentials to settle + await new Promise((r) => setTimeout(r, 200)); + + { + // third invocation group returns sequence-1 credentials, also with background refresh. + const credentials = await Promise.all([provider(), provider(), provider(), provider(), provider()]); + for (const c of credentials) { + expect(c).toEqual({ + accessKeyId: "", + secretAccessKey: "", + expiration, + sequence: 1, + runtimeOptions: [], + }); + } + expect(expiringCredentials).toHaveBeenCalledTimes(3); + } + }); +}); diff --git a/packages/credential-provider-node/src/runtime/memoize-chain.ts b/packages/credential-provider-node/src/runtime/memoize-chain.ts new file mode 100644 index 0000000000000..425c238d7f190 --- /dev/null +++ b/packages/credential-provider-node/src/runtime/memoize-chain.ts @@ -0,0 +1,74 @@ +import type { + AwsCredentialIdentity, + AwsIdentityProperties, + RuntimeConfigAwsCredentialIdentityProvider, +} from "@aws-sdk/types"; + +/** + * Memoized provider chain for AWS credentials. + * The options are only reevaluated if forceRefresh=true is passed or a natural + * refresh occurs. + * + * @public + */ +export interface MemoizedRuntimeConfigAwsCredentialIdentityProvider { + (options?: AwsIdentityProperties & { forceRefresh?: boolean }): Promise; +} + +/** + * @internal + */ +export function memoizeChain( + providers: RuntimeConfigAwsCredentialIdentityProvider[], + treatAsExpired: (resolved: AwsCredentialIdentity) => boolean +): MemoizedRuntimeConfigAwsCredentialIdentityProvider { + const chain = internalCreateChain(providers) as RuntimeConfigAwsCredentialIdentityProvider; + + // exists when fetching credentials and credentials have expired or don't exist. + let activeLock: Promise | undefined; + // active when fetching credentials and valid credentials still exist. + let passiveLock: Promise | undefined; + let credentials: AwsCredentialIdentity | undefined; + + const provider = async (options?: AwsIdentityProperties & { forceRefresh?: boolean }) => { + if (activeLock) { + await activeLock; + } else if (!credentials || treatAsExpired?.(credentials!)) { + if (credentials) { + if (!passiveLock) { + passiveLock = chain(options).then((c) => { + credentials = c; + passiveLock = undefined; + }); + } + } else { + activeLock = chain(options).then((c) => { + credentials = c; + activeLock = undefined; + }); + return provider(options); + } + } + return credentials!; + }; + + return provider; +} + +export const internalCreateChain = + (providers: RuntimeConfigAwsCredentialIdentityProvider[]): RuntimeConfigAwsCredentialIdentityProvider => + async (awsIdentityProperties?: AwsIdentityProperties) => { + let lastProviderError: Error | undefined; + for (const provider of providers) { + try { + return await provider(awsIdentityProperties); + } catch (err) { + lastProviderError = err; + if (err?.tryNextLink) { + continue; + } + throw err; + } + } + throw lastProviderError; + }; diff --git a/packages/credential-provider-node/tests/credential-provider-node.integ.spec.ts b/packages/credential-provider-node/tests/credential-provider-node.integ.spec.ts index 6c6970e7c8ce5..2667f30975cfb 100644 --- a/packages/credential-provider-node/tests/credential-provider-node.integ.spec.ts +++ b/packages/credential-provider-node/tests/credential-provider-node.integ.spec.ts @@ -1,25 +1,27 @@ -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test as it, vi } from "vitest"; -import { externalDataInterceptor } from "@smithy/shared-ini-file-loader"; -import { NodeHttpHandler } from "@smithy/node-http-handler"; import { STS, STSExtensionConfiguration } from "@aws-sdk/client-sts"; import * as credentialProviderHttp from "@aws-sdk/credential-provider-http"; import { fromCognitoIdentity, fromCognitoIdentityPool, fromIni, - fromWebToken, fromTokenFile, + fromWebToken, } from "@aws-sdk/credential-providers"; +import { fromNodeProviderChain } from "@aws-sdk/credential-providers"; +import { NodeHttpHandler } from "@smithy/node-http-handler"; import { HttpResponse } from "@smithy/protocol-http"; +import { externalDataInterceptor } from "@smithy/shared-ini-file-loader"; import type { HttpRequest, MiddlewareStack, NodeHttpHandlerOptions, ParsedIniData } from "@smithy/types"; import { AdaptiveRetryStrategy, StandardRetryStrategy } from "@smithy/util-retry"; -import { PassThrough } from "node:stream"; +import child_process from "node:child_process"; +import { createHash } from "node:crypto"; import { homedir } from "node:os"; import { join } from "node:path"; -import { createHash } from "node:crypto"; -import child_process from "node:child_process"; +import { PassThrough } from "node:stream"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test as it, vi } from "vitest"; -import { defaultProvider } from "../src"; +// eslint-disable-next-line no-restricted-imports +import { defaultProvider } from "../src/defaultProvider"; const assumeRoleArns: string[] = []; @@ -1442,7 +1444,7 @@ describe("credential-provider-node integration test", () => { }); describe("nested STS client", () => { - it("the clientConfig is propagated to the inner STS client used for AssumeRole ", async () => { + it("the clientConfig is propagated to the inner STS client used for AssumeRole", async () => { setIniProfileData({ assume: { region: "us-stsar-1", @@ -1625,5 +1627,181 @@ describe("credential-provider-node integration test", () => { }); }); }); + + describe("STS region logic", () => { + type Parameters = { + // has caller context client + withCaller: boolean; + // has region specified on the caller client + codeRegion: boolean; + // AWS_REGION is set + envRegion: boolean; + // profile regions are set + profileRegion: boolean; + // provider itself has a clientConfig.region + providerRegion: boolean; + // profile name + profile: string | undefined; + }; + + function serializeParams(params: Parameters) { + let buffer = ""; + for (const [key, value] of Object.entries(params)) { + if (typeof value === "boolean") { + if (value) { + buffer += ` ${key},`; + } + } else { + buffer += ` ${key} = ${value},`; + } + } + return buffer; + } + + for (const withCaller of [true, false]) { + for (const codeRegion of [true, false]) { + for (const envRegion of [true, false]) { + for (const profileRegion of [true, false]) { + for (const providerRegion of [true, false]) { + for (const profile of ["default", "alt", undefined]) { + if (!codeRegion && !profileRegion && !envRegion) { + continue; + } + + const params = { + withCaller, + codeRegion, + envRegion, + profileRegion, + providerRegion, + profile, + }; + + it(`${serializeParams(params)}`, async () => { + const region = await resolveStsRegion(params); + + if (providerRegion) { + expect(region).toBe("provider-region"); + return; + } + + if (profileRegion) { + expect(region).toBe(`${profile ?? "default"}-profile-region`); + return; + } + + if (codeRegion && withCaller) { + expect(region).toBe("code-region"); + return; + } + + if (envRegion) { + expect(region).toBe("env-region"); + return; + } + + expect(region).toBe("us-east-1"); + }); + } + } + } + } + } + } + + async function resolveStsRegion({ + withCaller, + envRegion, + profile, + profileRegion, + codeRegion, + providerRegion, + }: Parameters) { + if (envRegion) { + process.env.AWS_REGION = "env-region"; + } else { + delete process.env.AWS_REGION; + } + + if (profileRegion) { + iniProfileData = { + default: { + region: "default-profile-region", + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + external_id: "EXTERNAL_ID", + source_profile: "assume", + }, + assume: { + region: "assume-profile-region", + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + alt: { + region: "alt-profile-region", + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + external_id: "EXTERNAL_ID", + source_profile: "assume2", + }, + assume2: { + region: "assume2-profile-region", + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + }; + } else { + iniProfileData = { + default: { + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + external_id: "EXTERNAL_ID", + source_profile: "assume", + }, + assume: { + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + alt: { + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + external_id: "EXTERNAL_ID", + source_profile: "assume2", + }, + assume2: { + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + }; + } + setIniProfileData(iniProfileData); + + if (withCaller) { + const sts = new STS({ + profile, + region: codeRegion ? "code-region" : undefined, + credentials: fromNodeProviderChain({ + clientConfig: { + region: providerRegion ? "provider-region" : undefined, + }, + }), + }); + + await sts.getCallerIdentity({}); + const credentials = await sts.config.credentials(); + return credentials.sessionToken!.replace("STS_AR_SESSION_TOKEN_", ""); + } + + const provider = fromNodeProviderChain({ + profile, + clientConfig: { + region: providerRegion ? "provider-region" : undefined, + }, + }); + + const credentials = await provider(); + return credentials.sessionToken!.replace("STS_AR_SESSION_TOKEN_", ""); + } + }); }); }); diff --git a/packages/credential-providers/src/createCredentialChain.ts b/packages/credential-providers/src/createCredentialChain.ts index 795d5f6115b31..47e941cb4eff2 100644 --- a/packages/credential-providers/src/createCredentialChain.ts +++ b/packages/credential-providers/src/createCredentialChain.ts @@ -90,14 +90,13 @@ export const propertyProviderChain = (...providers: Array>): RuntimeConfigIdentityProvider => async (awsIdentityProperties?: AwsIdentityProperties) => { if (providers.length === 0) { - throw new ProviderError("No providers in chain"); + throw new ProviderError("No providers in chain", { tryNextLink: false }); } let lastProviderError: Error | undefined; for (const provider of providers) { try { - const credentials = await provider(awsIdentityProperties); - return credentials; + return await provider(awsIdentityProperties); } catch (err) { lastProviderError = err; if (err?.tryNextLink) { From 8eb9ff9e1789760df4d102701e4544bfa686611f Mon Sep 17 00:00:00 2001 From: George Fu Date: Tue, 28 Oct 2025 13:34:12 -0400 Subject: [PATCH 3/7] chore: forcerefresh --- .../credential-provider-node/package.json | 2 +- .../src/runtime/memoize-chain.spec.ts | 25 +++++++++++++++++++ .../src/runtime/memoize-chain.ts | 3 +++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/credential-provider-node/package.json b/packages/credential-provider-node/package.json index 4ab37c76aaf82..485cfdc316023 100644 --- a/packages/credential-provider-node/package.json +++ b/packages/credential-provider-node/package.json @@ -15,7 +15,7 @@ "build:types": "tsc -p tsconfig.types.json", "build:types:downlevel": "downlevel-dts dist-types dist-types/ts3.4", "clean": "rimraf ./dist-* && rimraf *.tsbuildinfo", - "test": "yarn g:vitest run", + "test": "yarn g:vitest run --reporter verbose", "test:watch": "yarn g:vitest watch", "test:integration": "yarn g:vitest run -c vitest.config.integ.mts", "test:integration:watch": "yarn g:vitest watch -c vitest.config.integ.mts" diff --git a/packages/credential-provider-node/src/runtime/memoize-chain.spec.ts b/packages/credential-provider-node/src/runtime/memoize-chain.spec.ts index 0817db9067148..7310491e27d86 100644 --- a/packages/credential-provider-node/src/runtime/memoize-chain.spec.ts +++ b/packages/credential-provider-node/src/runtime/memoize-chain.spec.ts @@ -131,4 +131,29 @@ describe("memoize runtime config aware AWS credential chain", () => { expect(expiringCredentials).toHaveBeenCalledTimes(3); } }); + + it("can be force refreshed", async () => { + const provider = memoizeChain([expiringCredentials], credentialsWillNeedRefresh); + + const credentials = await Promise.all([ + provider({ forceRefresh: true }), + provider({ forceRefresh: true }), + provider({ forceRefresh: true }), + provider({ forceRefresh: true }), + provider({ forceRefresh: true }), + ]); + let sequence = 0; + + for (const c of credentials) { + expect(c).toEqual({ + accessKeyId: "", + secretAccessKey: "", + expiration, + sequence: sequence++, + runtimeOptions: ["forceRefresh"], + }); + } + + expect(expiringCredentials).toHaveBeenCalledTimes(5); + }); }); diff --git a/packages/credential-provider-node/src/runtime/memoize-chain.ts b/packages/credential-provider-node/src/runtime/memoize-chain.ts index 425c238d7f190..2d9891a1a8956 100644 --- a/packages/credential-provider-node/src/runtime/memoize-chain.ts +++ b/packages/credential-provider-node/src/runtime/memoize-chain.ts @@ -31,6 +31,9 @@ export function memoizeChain( let credentials: AwsCredentialIdentity | undefined; const provider = async (options?: AwsIdentityProperties & { forceRefresh?: boolean }) => { + if (options?.forceRefresh) { + return await chain(options); + } if (activeLock) { await activeLock; } else if (!credentials || treatAsExpired?.(credentials!)) { From c3c5cb8d1ac59b6fd7bfdb3b01eedd07d5294559 Mon Sep 17 00:00:00 2001 From: George Fu Date: Tue, 28 Oct 2025 13:56:31 -0400 Subject: [PATCH 4/7] test: stub credential provder integ tests --- .../tests/_test-lib.spec.ts | 87 +++++++++++++++++++ .../tests/fromCognitoIdentity.integ.spec.ts | 6 ++ .../fromCognitoIdentityPool.integ.spec.ts | 6 ++ .../tests/fromContainerMetadata.integ.spec.ts | 6 ++ .../tests/fromEnv.integ.spec.ts | 6 ++ .../tests/fromHttp.integ.spec.ts | 6 ++ .../tests/fromIni.integ.spec.ts | 6 ++ .../tests/fromInstanceMetadata.integ.spec.ts | 6 ++ .../tests/fromNodeProviderChain.integ.spec.ts | 9 ++ .../tests/fromProcess.integ.spec.ts | 6 ++ .../tests/fromSSO.integ.spec.ts | 6 ++ .../fromTemporaryCredentials.integ.spec.ts | 6 ++ .../tests/fromTokenFile.integ.spec.ts | 6 ++ .../tests/fromWebToken.integ.spec.ts | 6 ++ 14 files changed, 168 insertions(+) create mode 100644 packages/credential-providers/tests/_test-lib.spec.ts create mode 100644 packages/credential-providers/tests/fromCognitoIdentity.integ.spec.ts create mode 100644 packages/credential-providers/tests/fromCognitoIdentityPool.integ.spec.ts create mode 100644 packages/credential-providers/tests/fromContainerMetadata.integ.spec.ts create mode 100644 packages/credential-providers/tests/fromEnv.integ.spec.ts create mode 100644 packages/credential-providers/tests/fromHttp.integ.spec.ts create mode 100644 packages/credential-providers/tests/fromIni.integ.spec.ts create mode 100644 packages/credential-providers/tests/fromInstanceMetadata.integ.spec.ts create mode 100644 packages/credential-providers/tests/fromNodeProviderChain.integ.spec.ts create mode 100644 packages/credential-providers/tests/fromProcess.integ.spec.ts create mode 100644 packages/credential-providers/tests/fromSSO.integ.spec.ts create mode 100644 packages/credential-providers/tests/fromTemporaryCredentials.integ.spec.ts create mode 100644 packages/credential-providers/tests/fromTokenFile.integ.spec.ts create mode 100644 packages/credential-providers/tests/fromWebToken.integ.spec.ts diff --git a/packages/credential-providers/tests/_test-lib.spec.ts b/packages/credential-providers/tests/_test-lib.spec.ts new file mode 100644 index 0000000000000..fdd5021416374 --- /dev/null +++ b/packages/credential-providers/tests/_test-lib.spec.ts @@ -0,0 +1,87 @@ +import { describe, test as it } from "vitest"; + +describe("placeholder for testing lib", () => { + it("", () => {}); +}); + +export type CredentialTestParameters = { + // has caller context client + withCaller: boolean; + // has region specified on the caller client + codeRegion: boolean; + // AWS_REGION is set + envRegion: boolean; + // profile regions are set + profileRegion: boolean; + // provider itself has a clientConfig.region + providerRegion: boolean; + // profile name + profile: string | undefined; +}; + +function serializeParams(params: CredentialTestParameters) { + let buffer = ""; + for (const [key, value] of Object.entries(params)) { + if (typeof value === "boolean") { + if (value) { + buffer += ` ${key},`; + } + } else { + buffer += ` ${key} = ${value},`; + } + } + return buffer; +} + +export function test() { + for (const withCaller of [true, false]) { + for (const codeRegion of [true, false]) { + for (const envRegion of [true, false]) { + for (const profileRegion of [true, false]) { + for (const providerRegion of [true, false]) { + for (const profile of ["default", "alt", undefined]) { + if (!codeRegion && !profileRegion && !envRegion) { + continue; + } + + const params = { + withCaller, + codeRegion, + envRegion, + profileRegion, + providerRegion, + profile, + }; + + it(`${serializeParams(params)}`, async () => { + // const region = await resolveStsRegion(params); + // + // if (providerRegion) { + // expect(region).toBe("provider-region"); + // return; + // } + // + // if (profileRegion) { + // expect(region).toBe(`${profile ?? "default"}-profile-region`); + // return; + // } + // + // if (codeRegion && withCaller) { + // expect(region).toBe("code-region"); + // return; + // } + // + // if (envRegion) { + // expect(region).toBe("env-region"); + // return; + // } + // + // expect(region).toBe("us-east-1"); + }); + } + } + } + } + } + } +} diff --git a/packages/credential-providers/tests/fromCognitoIdentity.integ.spec.ts b/packages/credential-providers/tests/fromCognitoIdentity.integ.spec.ts new file mode 100644 index 0000000000000..1a2d25fb0a85c --- /dev/null +++ b/packages/credential-providers/tests/fromCognitoIdentity.integ.spec.ts @@ -0,0 +1,6 @@ +import { fromCognitoIdentity } from "@aws-sdk/credential-providers"; +import { describe, test as it } from "vitest"; + +describe(fromCognitoIdentity.name, () => { + it("placeholder", () => {}); +}); diff --git a/packages/credential-providers/tests/fromCognitoIdentityPool.integ.spec.ts b/packages/credential-providers/tests/fromCognitoIdentityPool.integ.spec.ts new file mode 100644 index 0000000000000..ed4895a555d97 --- /dev/null +++ b/packages/credential-providers/tests/fromCognitoIdentityPool.integ.spec.ts @@ -0,0 +1,6 @@ +import { fromCognitoIdentityPool } from "@aws-sdk/credential-providers"; +import { describe, test as it } from "vitest"; + +describe(fromCognitoIdentityPool.name, () => { + it("placeholder", () => {}); +}); diff --git a/packages/credential-providers/tests/fromContainerMetadata.integ.spec.ts b/packages/credential-providers/tests/fromContainerMetadata.integ.spec.ts new file mode 100644 index 0000000000000..79a1fd998a8d4 --- /dev/null +++ b/packages/credential-providers/tests/fromContainerMetadata.integ.spec.ts @@ -0,0 +1,6 @@ +import { fromContainerMetadata } from "@aws-sdk/credential-providers"; +import { describe, test as it } from "vitest"; + +describe(fromContainerMetadata.name, () => { + it("placeholder", () => {}); +}); diff --git a/packages/credential-providers/tests/fromEnv.integ.spec.ts b/packages/credential-providers/tests/fromEnv.integ.spec.ts new file mode 100644 index 0000000000000..3d425bd0d5395 --- /dev/null +++ b/packages/credential-providers/tests/fromEnv.integ.spec.ts @@ -0,0 +1,6 @@ +import { fromEnv } from "@aws-sdk/credential-providers"; +import { describe, test as it } from "vitest"; + +describe(fromEnv.name, () => { + it("placeholder", () => {}); +}); diff --git a/packages/credential-providers/tests/fromHttp.integ.spec.ts b/packages/credential-providers/tests/fromHttp.integ.spec.ts new file mode 100644 index 0000000000000..3638276546b68 --- /dev/null +++ b/packages/credential-providers/tests/fromHttp.integ.spec.ts @@ -0,0 +1,6 @@ +import { fromHttp } from "@aws-sdk/credential-providers"; +import { describe, test as it } from "vitest"; + +describe(fromHttp.name, () => { + it("placeholder", () => {}); +}); diff --git a/packages/credential-providers/tests/fromIni.integ.spec.ts b/packages/credential-providers/tests/fromIni.integ.spec.ts new file mode 100644 index 0000000000000..b0aff03b5ec2d --- /dev/null +++ b/packages/credential-providers/tests/fromIni.integ.spec.ts @@ -0,0 +1,6 @@ +import { fromIni } from "@aws-sdk/credential-providers"; +import { describe, test as it } from "vitest"; + +describe(fromIni.name, () => { + it("placeholder", () => {}); +}); diff --git a/packages/credential-providers/tests/fromInstanceMetadata.integ.spec.ts b/packages/credential-providers/tests/fromInstanceMetadata.integ.spec.ts new file mode 100644 index 0000000000000..83bedb05f878e --- /dev/null +++ b/packages/credential-providers/tests/fromInstanceMetadata.integ.spec.ts @@ -0,0 +1,6 @@ +import { fromInstanceMetadata } from "@aws-sdk/credential-providers"; +import { describe, test as it } from "vitest"; + +describe(fromInstanceMetadata.name, () => { + it("placeholder", () => {}); +}); diff --git a/packages/credential-providers/tests/fromNodeProviderChain.integ.spec.ts b/packages/credential-providers/tests/fromNodeProviderChain.integ.spec.ts new file mode 100644 index 0000000000000..0a44764fdb565 --- /dev/null +++ b/packages/credential-providers/tests/fromNodeProviderChain.integ.spec.ts @@ -0,0 +1,9 @@ +import { fromNodeProviderChain } from "@aws-sdk/credential-providers"; +import { describe, test as it } from "vitest"; + +describe(fromNodeProviderChain.name, () => { + it("placeholder", () => { + // Most tests related to this provider are in the credential provider node integration suite. + // They can be moved here in the future, or separate tests can be written here. + }); +}); diff --git a/packages/credential-providers/tests/fromProcess.integ.spec.ts b/packages/credential-providers/tests/fromProcess.integ.spec.ts new file mode 100644 index 0000000000000..9be28a562b493 --- /dev/null +++ b/packages/credential-providers/tests/fromProcess.integ.spec.ts @@ -0,0 +1,6 @@ +import { fromProcess } from "@aws-sdk/credential-providers"; +import { describe, test as it } from "vitest"; + +describe(fromProcess.name, () => { + it("placeholder", () => {}); +}); diff --git a/packages/credential-providers/tests/fromSSO.integ.spec.ts b/packages/credential-providers/tests/fromSSO.integ.spec.ts new file mode 100644 index 0000000000000..6acbb2c0b9f2e --- /dev/null +++ b/packages/credential-providers/tests/fromSSO.integ.spec.ts @@ -0,0 +1,6 @@ +import { fromSSO } from "@aws-sdk/credential-providers"; +import { describe, test as it } from "vitest"; + +describe(fromSSO.name, () => { + it("placeholder", () => {}); +}); diff --git a/packages/credential-providers/tests/fromTemporaryCredentials.integ.spec.ts b/packages/credential-providers/tests/fromTemporaryCredentials.integ.spec.ts new file mode 100644 index 0000000000000..2759f79c1fe51 --- /dev/null +++ b/packages/credential-providers/tests/fromTemporaryCredentials.integ.spec.ts @@ -0,0 +1,6 @@ +import { fromTemporaryCredentials } from "@aws-sdk/credential-providers"; +import { describe, test as it } from "vitest"; + +describe(fromTemporaryCredentials.name, () => { + it("placeholder", () => {}); +}); diff --git a/packages/credential-providers/tests/fromTokenFile.integ.spec.ts b/packages/credential-providers/tests/fromTokenFile.integ.spec.ts new file mode 100644 index 0000000000000..0b70ae721d624 --- /dev/null +++ b/packages/credential-providers/tests/fromTokenFile.integ.spec.ts @@ -0,0 +1,6 @@ +import { fromTokenFile } from "@aws-sdk/credential-providers"; +import { describe, test as it } from "vitest"; + +describe(fromTokenFile.name, () => { + it("placeholder", () => {}); +}); diff --git a/packages/credential-providers/tests/fromWebToken.integ.spec.ts b/packages/credential-providers/tests/fromWebToken.integ.spec.ts new file mode 100644 index 0000000000000..b4fd1ffe8928a --- /dev/null +++ b/packages/credential-providers/tests/fromWebToken.integ.spec.ts @@ -0,0 +1,6 @@ +import { fromWebToken } from "@aws-sdk/credential-providers"; +import { describe, test as it } from "vitest"; + +describe(fromWebToken.name, () => { + it("placeholder", () => {}); +}); From a0c91ef4eb6c410e5de7aa357f2fe33dd5e9039c Mon Sep 17 00:00:00 2001 From: George Fu Date: Tue, 28 Oct 2025 14:42:04 -0400 Subject: [PATCH 5/7] chore: wip --- .../credential-provider-node.integ.spec.ts | 121 +---- .../tests/_test-lib.spec.ts | 477 ++++++++++++++++-- .../tests/fromEnv.integ.spec.ts | 6 + .../tests/fromIni.integ.spec.ts | 8 +- .../tests/fromNodeProviderChain.integ.spec.ts | 11 +- .../fromTemporaryCredentials.integ.spec.ts | 45 +- 6 files changed, 486 insertions(+), 182 deletions(-) diff --git a/packages/credential-provider-node/tests/credential-provider-node.integ.spec.ts b/packages/credential-provider-node/tests/credential-provider-node.integ.spec.ts index 2667f30975cfb..b362be4d98a1a 100644 --- a/packages/credential-provider-node/tests/credential-provider-node.integ.spec.ts +++ b/packages/credential-provider-node/tests/credential-provider-node.integ.spec.ts @@ -4,20 +4,19 @@ import { fromCognitoIdentity, fromCognitoIdentityPool, fromIni, + fromNodeProviderChain, fromTokenFile, fromWebToken, } from "@aws-sdk/credential-providers"; -import { fromNodeProviderChain } from "@aws-sdk/credential-providers"; +import { MockNodeHttpHandler } from "@aws-sdk/credential-providers/tests/_test-lib.spec"; import { NodeHttpHandler } from "@smithy/node-http-handler"; -import { HttpResponse } from "@smithy/protocol-http"; import { externalDataInterceptor } from "@smithy/shared-ini-file-loader"; -import type { HttpRequest, MiddlewareStack, NodeHttpHandlerOptions, ParsedIniData } from "@smithy/types"; +import type { HttpRequest, MiddlewareStack, ParsedIniData } from "@smithy/types"; import { AdaptiveRetryStrategy, StandardRetryStrategy } from "@smithy/util-retry"; import child_process from "node:child_process"; import { createHash } from "node:crypto"; import { homedir } from "node:os"; import { join } from "node:path"; -import { PassThrough } from "node:stream"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test as it, vi } from "vitest"; // eslint-disable-next-line no-restricted-imports @@ -25,120 +24,6 @@ import { defaultProvider } from "../src/defaultProvider"; const assumeRoleArns: string[] = []; -class MockNodeHttpHandler { - static create(instanceOrOptions?: any) { - if (typeof instanceOrOptions?.handle === "function") { - return instanceOrOptions; - } - return new MockNodeHttpHandler(); - } - - async handle(request: HttpRequest) { - const body = new PassThrough({}); - - if (request.body?.includes("RoleArn=")) { - assumeRoleArns.push(request.body.match(/RoleArn=(.*?)&/)?.[1]); - } - - const region = (request.hostname.match(/(sts|cognito-identity|portal\.sso)\.(.*?)\./) || [, , "unknown"])[2]; - - if (request.headers.Authorization === "container-authorization") { - body.write( - JSON.stringify({ - AccessKeyId: "CONTAINER_ACCESS_KEY", - SecretAccessKey: "CONTAINER_SECRET_ACCESS_KEY", - Token: "CONTAINER_TOKEN", - Expiration: "3000-01-01T00:00:00.000Z", - }) - ); - } else if (request.path?.includes("/federation/credentials")) { - body.write( - JSON.stringify({ - roleCredentials: { - accessKeyId: "SSO_ACCESS_KEY_ID", - secretAccessKey: "SSO_SECRET_ACCESS_KEY", - sessionToken: `SSO_SESSION_TOKEN_${region}`, - expiration: "3000-01-01T00:00:00.000Z", - }, - }) - ); - } else if (request.body?.includes("Action=AssumeRoleWithWebIdentity")) { - body.write(` - - - - STS_ARWI_ACCESS_KEY_ID - STS_ARWI_SECRET_ACCESS_KEY - STS_ARWI_SESSION_TOKEN_${region} - 3000-01-01T00:00:00.000Z - - - - 01234567-89ab-cdef-0123-456789abcdef - -`); - } else if (request.body?.includes("Action=AssumeRole")) { - body.write(` - - - - STS_AR_ACCESS_KEY_ID - STS_AR_SECRET_ACCESS_KEY - STS_AR_SESSION_TOKEN_${region} - 3000-01-01T00:00:00.000Z - - - - 01234567-89ab-cdef-0123-456789abcdef - -`); - } else if (request.body.includes("Action=GetCallerIdentity")) { - body.write(` - - - arn:aws:iam::123456789012:user/Alice - AIDACKCEVSQ6C2EXAMPLE - 123456789012 - - - 01234567-89ab-cdef-0123-456789abcdef - -`); - } else if (request.headers["x-amz-target"] === "AWSCognitoIdentityService.GetCredentialsForIdentity") { - body.write(`{ - "Credentials":{ - "SecretKey":"COGNITO_SECRET_KEY", - "SessionToken":"COGNITO_SESSION_TOKEN_${region}", - "Expiration":${new Date("3000-01-01T00:00:00.000Z").getTime() / 1000}, - "AccessKeyId":"COGNITO_ACCESS_KEY_ID" - }, - "IdentityId":"${region}:COGNITO_IDENTITY_ID" - }`); - } else if (request.headers["x-amz-target"] === "AWSCognitoIdentityService.GetId") { - body.write(`{ - "IdentityId":"${region}:COGNITO_IDENTITY_ID" - }`); - } else { - console.log(request); - throw new Error("request not supported."); - } - body.end(); - return { - response: new HttpResponse({ - statusCode: 200, - body, - headers: {}, - }), - }; - } - - updateHttpClientConfig(key: keyof NodeHttpHandlerOptions, value: NodeHttpHandlerOptions[typeof key]): void {} - - httpHandlerConfigs(): NodeHttpHandlerOptions { - return null as any; - } -} - describe("credential-provider-node integration test", () => { let sts: STS = null as any; let processSnapshot: typeof process.env = null as any; diff --git a/packages/credential-providers/tests/_test-lib.spec.ts b/packages/credential-providers/tests/_test-lib.spec.ts index fdd5021416374..336b117536329 100644 --- a/packages/credential-providers/tests/_test-lib.spec.ts +++ b/packages/credential-providers/tests/_test-lib.spec.ts @@ -1,14 +1,29 @@ -import { describe, test as it } from "vitest"; +import { S3 } from "@aws-sdk/client-s3"; +import { ParsedIniData, RuntimeConfigAwsCredentialIdentityProvider } from "@aws-sdk/types"; +import { AttributedAwsCredentialIdentity } from "@aws-sdk/types/src"; +import { NodeHttpHandler } from "@smithy/node-http-handler"; +import { HttpResponse } from "@smithy/protocol-http"; +import { externalDataInterceptor } from "@smithy/shared-ini-file-loader"; +import type { HttpRequest, NodeHttpHandlerOptions } from "@smithy/types"; +import child_process from "node:child_process"; +import { createHash } from "node:crypto"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import { PassThrough } from "node:stream"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test as it } from "vitest"; describe("placeholder for testing lib", () => { it("", () => {}); }); +const assumeRoleArns: string[] = []; +let iniProfileData: ParsedIniData = null as any; + export type CredentialTestParameters = { // has caller context client withCaller: boolean; // has region specified on the caller client - codeRegion: boolean; + callerClientRegion: boolean; // AWS_REGION is set envRegion: boolean; // profile regions are set @@ -19,6 +34,411 @@ export type CredentialTestParameters = { profile: string | undefined; }; +/** + * Credential provider tester. + */ +export class CTest

RuntimeConfigAwsCredentialIdentityProvider> { + private lastCredentials: AttributedAwsCredentialIdentity | undefined; + + public constructor( + public credentialProvider: P, + public providerParams: (testParams: CredentialTestParameters) => Parameters

[0], + public profileCredentials?: boolean + ) { + this.init(); + } + + public static defaultRegionConfigProvider({ profile, providerRegion, withCaller }: CredentialTestParameters) { + if (withCaller) { + return { + clientConfig: { + region: providerRegion ? "provider-region" : undefined, + }, + }; + } + return { + profile, + clientConfig: { + region: providerRegion ? "provider-region" : undefined, + }, + }; + } + + public init() { + let processSnapshot: typeof process.env = null as any; + const nodeHttpHandlerCreate = NodeHttpHandler.create; + + const RESERVED_ENVIRONMENT_VARIABLES = { + AWS_DEFAULT_REGION: 1, + AWS_REGION: 1, + AWS_PROFILE: 1, + AWS_ACCESS_KEY_ID: 1, + AWS_SECRET_ACCESS_KEY: 1, + AWS_SESSION_TOKEN: 1, + AWS_CREDENTIAL_EXPIRATION: 1, + AWS_EC2_METADATA_DISABLED: 1, + AWS_WEB_IDENTITY_TOKEN_FILE: 1, + AWS_ROLE_ARN: 1, + AWS_CONTAINER_CREDENTIALS_FULL_URI: 1, + AWS_CONTAINER_CREDENTIALS_RELATIVE_URI: 1, + AWS_CONTAINER_AUTHORIZATION_TOKEN: 1, + AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE: 1, + }; + + function copy(data: T): T { + return JSON.parse(JSON.stringify(data)); + } + + beforeAll(async () => { + processSnapshot = copy(process.env); + NodeHttpHandler.create = MockNodeHttpHandler.create; + const mockExec = ((bin: string, ...args: any[]) => { + const callback = args.find((arg) => typeof arg === "function"); + if (bin === "credential-process") { + return callback(null, { + stdout: JSON.stringify({ + Version: 1, + AccessKeyId: "PROCESS_ACCESS_KEY_ID", + SecretAccessKey: "PROCESS_SECRET_ACCESS_KEY", + SessionToken: "PROCESS_SESSION_TOKEN", + }), + }); + } + return child_process.exec(bin, ...args); + }) as any; + + externalDataInterceptor.interceptToken("exec", mockExec); + }); + + beforeEach(async () => { + for (const variable in RESERVED_ENVIRONMENT_VARIABLES) { + delete process.env[variable]; + } + setIniProfileData({ + default: { + region: "us-west-2", + output: "json", + }, + }); + const dir = join(homedir(), ".aws"); + externalDataInterceptor.interceptFile(join(dir, "credentials"), ""); + externalDataInterceptor.interceptFile("token-filepath", "token-contents"); + const ssoToken = { + accessToken: "mock_sso_token", + expiresAt: "3000-01-01T00:00:00.000Z", + }; + const hasher = createHash("sha1"); + const cacheName = hasher.update("SSO_START_URL").digest("hex"); + const tokenPath = join(homedir(), ".aws", "sso", "cache", `${cacheName}.json`); + externalDataInterceptor.interceptFile(tokenPath, JSON.stringify(ssoToken)); + externalDataInterceptor.interceptToken("SSO_START_URL", ssoToken); + externalDataInterceptor.interceptToken("ssoNew", ssoToken); + externalDataInterceptor.interceptToken("token-filepath", "token-contents"); + }); + + afterEach(async () => { + Object.assign(process.env, processSnapshot); + setIniProfileData({ + default: {}, + }); + assumeRoleArns.length = 0; + }); + + afterAll(() => { + NodeHttpHandler.create = nodeHttpHandlerCreate; + delete externalDataInterceptor.getTokenRecord().exec; + }); + } + + public setIni(data: Parameters[0]) { + setIniProfileData(data); + } + + public testRegion() { + for (const withCaller of [true, false]) { + for (const callerClientRegion of [true, false]) { + for (const envRegion of [true, false]) { + for (const profileRegion of [true, false]) { + for (const providerRegion of [true, false]) { + for (const profile of ["default", "alt", undefined]) { + if (!callerClientRegion && !profileRegion && !envRegion) { + continue; + } + + const params = { + withCaller, + callerClientRegion, + envRegion, + profileRegion, + providerRegion, + profile, + }; + + it(`${serializeParams(params)}`, async () => { + const region = await this.resolveStsRegion(params); + + if (providerRegion) { + expect(region).toBe("provider-region"); + return; + } + + const usesProfileCredentials = this.profileCredentials; + + if (usesProfileCredentials && profileRegion) { + expect(region).toBe(`${profile ?? "default"}-profile-region`); + return; + } + + if (callerClientRegion && withCaller) { + expect(region).toBe("code-region"); + return; + } + + if (envRegion) { + expect(region).toBe("env-region"); + return; + } + + if (!usesProfileCredentials && profileRegion) { + expect(region).toBe(`${profile ?? "default"}-profile-region`); + return; + } + + expect(region).toBe("us-east-1"); + }); + } + } + } + } + } + } + } + + private async resolveStsRegion(testParams: CredentialTestParameters) { + const { withCaller, envRegion, profile, profileRegion, callerClientRegion, providerRegion } = testParams; + + if (envRegion) { + process.env.AWS_REGION = "env-region"; + } else { + delete process.env.AWS_REGION; + } + + if (profileRegion) { + iniProfileData = { + default: { + region: "default-profile-region", + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + external_id: "EXTERNAL_ID", + source_profile: "assume", + }, + assume: { + region: "assume-profile-region", + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + alt: { + region: "alt-profile-region", + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + external_id: "EXTERNAL_ID", + source_profile: "assume2", + }, + assume2: { + region: "assume2-profile-region", + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + }; + } else { + iniProfileData = { + default: { + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + external_id: "EXTERNAL_ID", + source_profile: "assume", + }, + assume: { + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + alt: { + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + external_id: "EXTERNAL_ID", + source_profile: "assume2", + }, + assume2: { + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + }; + } + setIniProfileData(iniProfileData); + + if (withCaller) { + const s3 = new S3({ + profile, + region: callerClientRegion ? "code-region" : undefined, + credentials: this.credentialProvider(this.providerParams(testParams)), + }); + + await s3.listBuckets({}); + const credentials = await s3.config.credentials(); + return credentials.sessionToken!.replace("STS_AR_SESSION_TOKEN_", ""); + } + + const provider = this.credentialProvider(this.providerParams(testParams)); + + const credentials = await provider(); + return credentials.sessionToken!.replace("STS_AR_SESSION_TOKEN_", ""); + } +} + +export class MockNodeHttpHandler { + static create(instanceOrOptions?: any) { + if (typeof instanceOrOptions?.handle === "function") { + return instanceOrOptions; + } + return new MockNodeHttpHandler(); + } + + async handle(request: HttpRequest) { + const body = new PassThrough({}); + + if (request.body?.includes("RoleArn=")) { + assumeRoleArns.push(request.body.match(/RoleArn=(.*?)&/)?.[1]); + } + + const region = (request.hostname.match(/(sts|cognito-identity|portal\.sso)\.(.*?)\./) || [, , "unknown"])[2]; + + if (request.headers.Authorization === "container-authorization") { + body.write( + JSON.stringify({ + AccessKeyId: "CONTAINER_ACCESS_KEY", + SecretAccessKey: "CONTAINER_SECRET_ACCESS_KEY", + Token: "CONTAINER_TOKEN", + Expiration: "3000-01-01T00:00:00.000Z", + }) + ); + } else if (request.path?.includes("/federation/credentials")) { + body.write( + JSON.stringify({ + roleCredentials: { + accessKeyId: "SSO_ACCESS_KEY_ID", + secretAccessKey: "SSO_SECRET_ACCESS_KEY", + sessionToken: `SSO_SESSION_TOKEN_${region}`, + expiration: "3000-01-01T00:00:00.000Z", + }, + }) + ); + } else if (request.body?.includes("Action=AssumeRoleWithWebIdentity")) { + body.write(` + + + + STS_ARWI_ACCESS_KEY_ID + STS_ARWI_SECRET_ACCESS_KEY + STS_ARWI_SESSION_TOKEN_${region} + 3000-01-01T00:00:00.000Z + + + + 01234567-89ab-cdef-0123-456789abcdef + +`); + } else if (request.body?.includes("Action=AssumeRole")) { + body.write(` + + + + STS_AR_ACCESS_KEY_ID + STS_AR_SECRET_ACCESS_KEY + STS_AR_SESSION_TOKEN_${region} + 3000-01-01T00:00:00.000Z + + + + 01234567-89ab-cdef-0123-456789abcdef + +`); + } else if (request.body?.includes("Action=GetCallerIdentity")) { + body.write(` + + + arn:aws:iam::123456789012:user/Alice + AIDACKCEVSQ6C2EXAMPLE + 123456789012 + + + 01234567-89ab-cdef-0123-456789abcdef + +`); + } else if (request.headers["x-amz-target"] === "AWSCognitoIdentityService.GetCredentialsForIdentity") { + body.write(`{ + "Credentials":{ + "SecretKey":"COGNITO_SECRET_KEY", + "SessionToken":"COGNITO_SESSION_TOKEN_${region}", + "Expiration":${new Date("3000-01-01T00:00:00.000Z").getTime() / 1000}, + "AccessKeyId":"COGNITO_ACCESS_KEY_ID" + }, + "IdentityId":"${region}:COGNITO_IDENTITY_ID" + }`); + } else if (request.headers["x-amz-target"] === "AWSCognitoIdentityService.GetId") { + body.write(`{ + "IdentityId":"${region}:COGNITO_IDENTITY_ID" + }`); + } else if (request.hostname.startsWith("s3.")) { + body.write(` + + + + + xx + xx + + xx + xx +`); + } else { + console.log(request); + throw new Error("request not supported."); + } + body.end(); + return { + response: new HttpResponse({ + statusCode: 200, + body, + headers: {}, + }), + }; + } + + updateHttpClientConfig(key: keyof NodeHttpHandlerOptions, value: NodeHttpHandlerOptions[typeof key]): void {} + + httpHandlerConfigs(): NodeHttpHandlerOptions { + return null as any; + } +} + +function setIniProfileData(data: ParsedIniData) { + iniProfileData = data; + let buffer = "[profile memfs-test-mock]\n\n"; + for (const profile in data) { + if (profile.startsWith("sso-session.")) { + buffer += `[sso-session ${profile.split("sso-session.")[1]}]\n`; + } else { + buffer += `[profile ${profile}]\n`; + } + for (const [k, v] of Object.entries(data[profile])) { + buffer += `${k} = ${v}\n`; + } + buffer += "\n"; + } + const dir = join(homedir(), ".aws"); + externalDataInterceptor.interceptFile(join(dir, "config"), buffer); +} + function serializeParams(params: CredentialTestParameters) { let buffer = ""; for (const [key, value] of Object.entries(params)) { @@ -32,56 +452,3 @@ function serializeParams(params: CredentialTestParameters) { } return buffer; } - -export function test() { - for (const withCaller of [true, false]) { - for (const codeRegion of [true, false]) { - for (const envRegion of [true, false]) { - for (const profileRegion of [true, false]) { - for (const providerRegion of [true, false]) { - for (const profile of ["default", "alt", undefined]) { - if (!codeRegion && !profileRegion && !envRegion) { - continue; - } - - const params = { - withCaller, - codeRegion, - envRegion, - profileRegion, - providerRegion, - profile, - }; - - it(`${serializeParams(params)}`, async () => { - // const region = await resolveStsRegion(params); - // - // if (providerRegion) { - // expect(region).toBe("provider-region"); - // return; - // } - // - // if (profileRegion) { - // expect(region).toBe(`${profile ?? "default"}-profile-region`); - // return; - // } - // - // if (codeRegion && withCaller) { - // expect(region).toBe("code-region"); - // return; - // } - // - // if (envRegion) { - // expect(region).toBe("env-region"); - // return; - // } - // - // expect(region).toBe("us-east-1"); - }); - } - } - } - } - } - } -} diff --git a/packages/credential-providers/tests/fromEnv.integ.spec.ts b/packages/credential-providers/tests/fromEnv.integ.spec.ts index 3d425bd0d5395..fc5c4e0d99f21 100644 --- a/packages/credential-providers/tests/fromEnv.integ.spec.ts +++ b/packages/credential-providers/tests/fromEnv.integ.spec.ts @@ -1,6 +1,12 @@ import { fromEnv } from "@aws-sdk/credential-providers"; import { describe, test as it } from "vitest"; +import { CTest } from "./_test-lib.spec"; + describe(fromEnv.name, () => { + const ctest = new CTest(fromEnv, () => { + return {}; + }); + it("placeholder", () => {}); }); diff --git a/packages/credential-providers/tests/fromIni.integ.spec.ts b/packages/credential-providers/tests/fromIni.integ.spec.ts index b0aff03b5ec2d..fceb8b6bceaa0 100644 --- a/packages/credential-providers/tests/fromIni.integ.spec.ts +++ b/packages/credential-providers/tests/fromIni.integ.spec.ts @@ -1,6 +1,10 @@ import { fromIni } from "@aws-sdk/credential-providers"; -import { describe, test as it } from "vitest"; +import { describe } from "vitest"; + +import { CTest } from "./_test-lib.spec"; describe(fromIni.name, () => { - it("placeholder", () => {}); + const ctest = new CTest(fromIni, CTest.defaultRegionConfigProvider, true); + + ctest.testRegion(); }); diff --git a/packages/credential-providers/tests/fromNodeProviderChain.integ.spec.ts b/packages/credential-providers/tests/fromNodeProviderChain.integ.spec.ts index 0a44764fdb565..3d75d16edddef 100644 --- a/packages/credential-providers/tests/fromNodeProviderChain.integ.spec.ts +++ b/packages/credential-providers/tests/fromNodeProviderChain.integ.spec.ts @@ -1,9 +1,10 @@ import { fromNodeProviderChain } from "@aws-sdk/credential-providers"; -import { describe, test as it } from "vitest"; +import { describe } from "vitest"; + +import { CTest } from "./_test-lib.spec"; describe(fromNodeProviderChain.name, () => { - it("placeholder", () => { - // Most tests related to this provider are in the credential provider node integration suite. - // They can be moved here in the future, or separate tests can be written here. - }); + const ctest = new CTest(fromNodeProviderChain, CTest.defaultRegionConfigProvider, true); + + ctest.testRegion(); }); diff --git a/packages/credential-providers/tests/fromTemporaryCredentials.integ.spec.ts b/packages/credential-providers/tests/fromTemporaryCredentials.integ.spec.ts index 2759f79c1fe51..821e24833277a 100644 --- a/packages/credential-providers/tests/fromTemporaryCredentials.integ.spec.ts +++ b/packages/credential-providers/tests/fromTemporaryCredentials.integ.spec.ts @@ -1,6 +1,47 @@ +import { S3 } from "@aws-sdk/client-s3"; import { fromTemporaryCredentials } from "@aws-sdk/credential-providers"; -import { describe, test as it } from "vitest"; +import { describe, expect, test as it } from "vitest"; + +import { CTest } from "./_test-lib.spec"; describe(fromTemporaryCredentials.name, () => { - it("placeholder", () => {}); + const ctest = new CTest( + fromTemporaryCredentials, + (testParams) => { + return { + params: { + RoleArn: "arn:aws:iam::1234567890:role/Rigamarole", + }, + ...CTest.defaultRegionConfigProvider(testParams), + }; + }, + false + ); + + ctest.testRegion(); + + it("should resolve region", async () => { + ctest.setIni({ + alt: { + region: "us-east-2", + }, + }); + + const s3 = new S3({ + profile: "alt", + credentials: fromTemporaryCredentials({ + masterCredentials: { + accessKeyId: "M", + secretAccessKey: "M", + }, + params: { + RoleArn: "arn:aws:iam::1234567890:role/Rigamarole", + }, + }), + }); + + expect(await s3.config.credentials()).toMatchObject({ + sessionToken: "STS_AR_SESSION_TOKEN_us-east-2", + }); + }); }); From 444d7bd1c821e218f0c104c7d12f520b353f41bc Mon Sep 17 00:00:00 2001 From: George Fu Date: Wed, 29 Oct 2025 00:01:05 -0400 Subject: [PATCH 6/7] test(credential-providers)): region suite --- .../credential-provider-node.integ.spec.ts | 4 +- packages/credential-providers/README.md | 2 +- .../tests/_test-lib.spec.ts | 70 +++++++++++++++---- .../tests/fromCognitoIdentity.integ.spec.ts | 18 ++++- .../fromCognitoIdentityPool.integ.spec.ts | 18 ++++- .../tests/fromEnv.integ.spec.ts | 7 +- .../tests/fromIni.integ.spec.ts | 7 +- .../tests/fromNodeProviderChain.integ.spec.ts | 7 +- .../tests/fromSSO.integ.spec.ts | 21 +++++- .../fromTemporaryCredentials.integ.spec.ts | 11 +-- .../tests/fromTokenFile.integ.spec.ts | 19 ++++- .../tests/fromWebToken.integ.spec.ts | 19 ++++- 12 files changed, 166 insertions(+), 37 deletions(-) diff --git a/packages/credential-provider-node/tests/credential-provider-node.integ.spec.ts b/packages/credential-provider-node/tests/credential-provider-node.integ.spec.ts index b362be4d98a1a..af6eede709ace 100644 --- a/packages/credential-provider-node/tests/credential-provider-node.integ.spec.ts +++ b/packages/credential-provider-node/tests/credential-provider-node.integ.spec.ts @@ -8,7 +8,7 @@ import { fromTokenFile, fromWebToken, } from "@aws-sdk/credential-providers"; -import { MockNodeHttpHandler } from "@aws-sdk/credential-providers/tests/_test-lib.spec"; +import { MockNodeHttpHandler, assumeRoleArns } from "@aws-sdk/credential-providers/tests/_test-lib.spec"; import { NodeHttpHandler } from "@smithy/node-http-handler"; import { externalDataInterceptor } from "@smithy/shared-ini-file-loader"; import type { HttpRequest, MiddlewareStack, ParsedIniData } from "@smithy/types"; @@ -22,8 +22,6 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test as i // eslint-disable-next-line no-restricted-imports import { defaultProvider } from "../src/defaultProvider"; -const assumeRoleArns: string[] = []; - describe("credential-provider-node integration test", () => { let sts: STS = null as any; let processSnapshot: typeof process.env = null as any; diff --git a/packages/credential-providers/README.md b/packages/credential-providers/README.md index c9faebdf3812a..1d2fadfb31544 100644 --- a/packages/credential-providers/README.md +++ b/packages/credential-providers/README.md @@ -826,7 +826,7 @@ import { fromSSO } from "@aws-sdk/credential-providers"; // ES6 import const client = new FooClient({ // Optional, available on clients as of v3.714.0. profile: "my-sso-profile", - credentials: fromProcess({ + credentials: fromSSO({ // Optional. Defaults to the client's profile if that is set. // You can specify a profile here as well, but this applies // only to the credential resolution and not to the upper client. diff --git a/packages/credential-providers/tests/_test-lib.spec.ts b/packages/credential-providers/tests/_test-lib.spec.ts index 336b117536329..946f26f97b68a 100644 --- a/packages/credential-providers/tests/_test-lib.spec.ts +++ b/packages/credential-providers/tests/_test-lib.spec.ts @@ -1,6 +1,5 @@ import { S3 } from "@aws-sdk/client-s3"; import { ParsedIniData, RuntimeConfigAwsCredentialIdentityProvider } from "@aws-sdk/types"; -import { AttributedAwsCredentialIdentity } from "@aws-sdk/types/src"; import { NodeHttpHandler } from "@smithy/node-http-handler"; import { HttpResponse } from "@smithy/protocol-http"; import { externalDataInterceptor } from "@smithy/shared-ini-file-loader"; @@ -11,12 +10,13 @@ import { homedir } from "node:os"; import { join } from "node:path"; import { PassThrough } from "node:stream"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test as it } from "vitest"; +import { fromSSO } from "@aws-sdk/credential-providers"; describe("placeholder for testing lib", () => { it("", () => {}); }); -const assumeRoleArns: string[] = []; +export const assumeRoleArns: string[] = []; let iniProfileData: ParsedIniData = null as any; export type CredentialTestParameters = { @@ -38,13 +38,30 @@ export type CredentialTestParameters = { * Credential provider tester. */ export class CTest

RuntimeConfigAwsCredentialIdentityProvider> { - private lastCredentials: AttributedAwsCredentialIdentity | undefined; - - public constructor( - public credentialProvider: P, - public providerParams: (testParams: CredentialTestParameters) => Parameters

[0], - public profileCredentials?: boolean - ) { + private readonly credentialProvider: P; + private readonly providerParams: (testParams: CredentialTestParameters) => Parameters

[0]; + private readonly profileCredentials: boolean; + private readonly filter: (testParams: CredentialTestParameters) => boolean; + private readonly fallbackRegion: string; + + public constructor({ + credentialProvider, + providerParams, + profileCredentials, + filter, + fallbackRegion, + }: { + credentialProvider: P; + providerParams: (testParams: CredentialTestParameters) => Parameters

[0]; + profileCredentials?: boolean; + filter?: (testParams: CredentialTestParameters) => boolean; + fallbackRegion?: string; + }) { + this.credentialProvider = credentialProvider; + this.providerParams = providerParams; + this.profileCredentials = !!profileCredentials; + this.filter = filter ?? (() => true); + this.fallbackRegion = fallbackRegion ?? "unresolved"; this.init(); } @@ -57,8 +74,11 @@ export class CTest

RuntimeConfigAwsCredentialIdentityP }; } return { + // used by fromIni profile, clientConfig: { + // used by e.g. fromTemporaryCredentials that don't have top level profile selection + profile, region: providerRegion ? "provider-region" : undefined, }, }; @@ -157,6 +177,9 @@ export class CTest

RuntimeConfigAwsCredentialIdentityP public testRegion() { for (const withCaller of [true, false]) { for (const callerClientRegion of [true, false]) { + if (callerClientRegion && !withCaller) { + continue; + } for (const envRegion of [true, false]) { for (const profileRegion of [true, false]) { for (const providerRegion of [true, false]) { @@ -174,14 +197,33 @@ export class CTest

RuntimeConfigAwsCredentialIdentityP profile, }; + if (!this.filter(params)) { + continue; + } + it(`${serializeParams(params)}`, async () => { - const region = await this.resolveStsRegion(params); + const region = await this.findCredentialSourceRegion(params).catch((e) => { + return "failed"; + }); + const regionRequired = this.fallbackRegion === "unresolved" || withCaller; + const providerParams = this.providerParams(params); + const isSso = this.credentialProvider === fromSSO || providerParams.ssoStartUrl; + const hasRegion = providerRegion || profileRegion || callerClientRegion || envRegion; + + if (regionRequired && !hasRegion) { + expect(region).toBe("failed"); + } if (providerRegion) { expect(region).toBe("provider-region"); return; } + if (isSso) { + expect(region).toBe(providerParams.ssoRegion); + return; + } + const usesProfileCredentials = this.profileCredentials; if (usesProfileCredentials && profileRegion) { @@ -204,7 +246,7 @@ export class CTest

RuntimeConfigAwsCredentialIdentityP return; } - expect(region).toBe("us-east-1"); + expect(region).toBe(this.fallbackRegion); }); } } @@ -214,7 +256,7 @@ export class CTest

RuntimeConfigAwsCredentialIdentityP } } - private async resolveStsRegion(testParams: CredentialTestParameters) { + private async findCredentialSourceRegion(testParams: CredentialTestParameters) { const { withCaller, envRegion, profile, profileRegion, callerClientRegion, providerRegion } = testParams; if (envRegion) { @@ -285,13 +327,13 @@ export class CTest

RuntimeConfigAwsCredentialIdentityP await s3.listBuckets({}); const credentials = await s3.config.credentials(); - return credentials.sessionToken!.replace("STS_AR_SESSION_TOKEN_", ""); + return credentials.sessionToken!.replace(/(.*?)SESSION_TOKEN_/, ""); } const provider = this.credentialProvider(this.providerParams(testParams)); const credentials = await provider(); - return credentials.sessionToken!.replace("STS_AR_SESSION_TOKEN_", ""); + return credentials.sessionToken!.replace(/(.*?)SESSION_TOKEN_/, ""); } } diff --git a/packages/credential-providers/tests/fromCognitoIdentity.integ.spec.ts b/packages/credential-providers/tests/fromCognitoIdentity.integ.spec.ts index 1a2d25fb0a85c..2cb1847cb8917 100644 --- a/packages/credential-providers/tests/fromCognitoIdentity.integ.spec.ts +++ b/packages/credential-providers/tests/fromCognitoIdentity.integ.spec.ts @@ -1,6 +1,20 @@ import { fromCognitoIdentity } from "@aws-sdk/credential-providers"; -import { describe, test as it } from "vitest"; +import { describe } from "vitest"; + +import { CTest } from "./_test-lib.spec"; describe(fromCognitoIdentity.name, () => { - it("placeholder", () => {}); + const ctest = new CTest({ + credentialProvider: fromCognitoIdentity, + providerParams: (testParams) => { + return { + identityId: "us-east-1:128d0a74-c82f-4553-916d-90053example", + ...CTest.defaultRegionConfigProvider(testParams), + }; + }, + profileCredentials: false, + fallbackRegion: "unresolved", + }); + + ctest.testRegion(); }); diff --git a/packages/credential-providers/tests/fromCognitoIdentityPool.integ.spec.ts b/packages/credential-providers/tests/fromCognitoIdentityPool.integ.spec.ts index ed4895a555d97..62f4c9dac2624 100644 --- a/packages/credential-providers/tests/fromCognitoIdentityPool.integ.spec.ts +++ b/packages/credential-providers/tests/fromCognitoIdentityPool.integ.spec.ts @@ -1,6 +1,20 @@ import { fromCognitoIdentityPool } from "@aws-sdk/credential-providers"; -import { describe, test as it } from "vitest"; +import { describe } from "vitest"; + +import { CTest } from "./_test-lib.spec"; describe(fromCognitoIdentityPool.name, () => { - it("placeholder", () => {}); + const ctest = new CTest({ + credentialProvider: fromCognitoIdentityPool, + providerParams: (testParams) => { + return { + identityPoolId: "us-east-1:1699ebc0-7900-4099-b910-2df94f52a030", + ...CTest.defaultRegionConfigProvider(testParams), + }; + }, + profileCredentials: false, + fallbackRegion: "unresolved", + }); + + ctest.testRegion(); }); diff --git a/packages/credential-providers/tests/fromEnv.integ.spec.ts b/packages/credential-providers/tests/fromEnv.integ.spec.ts index fc5c4e0d99f21..2d2b627b5a1e8 100644 --- a/packages/credential-providers/tests/fromEnv.integ.spec.ts +++ b/packages/credential-providers/tests/fromEnv.integ.spec.ts @@ -4,8 +4,11 @@ import { describe, test as it } from "vitest"; import { CTest } from "./_test-lib.spec"; describe(fromEnv.name, () => { - const ctest = new CTest(fromEnv, () => { - return {}; + const ctest = new CTest({ + credentialProvider: fromEnv, + providerParams: () => { + return {}; + }, }); it("placeholder", () => {}); diff --git a/packages/credential-providers/tests/fromIni.integ.spec.ts b/packages/credential-providers/tests/fromIni.integ.spec.ts index fceb8b6bceaa0..b465422c00dbd 100644 --- a/packages/credential-providers/tests/fromIni.integ.spec.ts +++ b/packages/credential-providers/tests/fromIni.integ.spec.ts @@ -4,7 +4,12 @@ import { describe } from "vitest"; import { CTest } from "./_test-lib.spec"; describe(fromIni.name, () => { - const ctest = new CTest(fromIni, CTest.defaultRegionConfigProvider, true); + const ctest = new CTest({ + credentialProvider: fromIni, + providerParams: CTest.defaultRegionConfigProvider, + profileCredentials: true, + fallbackRegion: "us-east-1", + }); ctest.testRegion(); }); diff --git a/packages/credential-providers/tests/fromNodeProviderChain.integ.spec.ts b/packages/credential-providers/tests/fromNodeProviderChain.integ.spec.ts index 3d75d16edddef..7c2f70b4500b2 100644 --- a/packages/credential-providers/tests/fromNodeProviderChain.integ.spec.ts +++ b/packages/credential-providers/tests/fromNodeProviderChain.integ.spec.ts @@ -4,7 +4,12 @@ import { describe } from "vitest"; import { CTest } from "./_test-lib.spec"; describe(fromNodeProviderChain.name, () => { - const ctest = new CTest(fromNodeProviderChain, CTest.defaultRegionConfigProvider, true); + const ctest = new CTest({ + credentialProvider: fromNodeProviderChain, + providerParams: CTest.defaultRegionConfigProvider, + profileCredentials: true, + fallbackRegion: "us-east-1", + }); ctest.testRegion(); }); diff --git a/packages/credential-providers/tests/fromSSO.integ.spec.ts b/packages/credential-providers/tests/fromSSO.integ.spec.ts index 6acbb2c0b9f2e..4057e4c9f638e 100644 --- a/packages/credential-providers/tests/fromSSO.integ.spec.ts +++ b/packages/credential-providers/tests/fromSSO.integ.spec.ts @@ -1,6 +1,23 @@ import { fromSSO } from "@aws-sdk/credential-providers"; -import { describe, test as it } from "vitest"; +import { describe } from "vitest"; + +import { CTest } from "./_test-lib.spec"; describe(fromSSO.name, () => { - it("placeholder", () => {}); + const ctest = new CTest({ + credentialProvider: fromSSO, + providerParams: (testParams) => { + return { + ssoStartUrl: "SSO_START_URL", + ssoAccountId: "1234567890", + ssoRegion: "sso-region-1", + ssoRoleName: "arn:aws:iam::1234567890:role/Rigamarole", + ...CTest.defaultRegionConfigProvider(testParams), + }; + }, + profileCredentials: false, + fallbackRegion: "unresolved", + }); + + ctest.testRegion(); }); diff --git a/packages/credential-providers/tests/fromTemporaryCredentials.integ.spec.ts b/packages/credential-providers/tests/fromTemporaryCredentials.integ.spec.ts index 821e24833277a..605ccfc2fc783 100644 --- a/packages/credential-providers/tests/fromTemporaryCredentials.integ.spec.ts +++ b/packages/credential-providers/tests/fromTemporaryCredentials.integ.spec.ts @@ -5,9 +5,9 @@ import { describe, expect, test as it } from "vitest"; import { CTest } from "./_test-lib.spec"; describe(fromTemporaryCredentials.name, () => { - const ctest = new CTest( - fromTemporaryCredentials, - (testParams) => { + const ctest = new CTest({ + credentialProvider: fromTemporaryCredentials, + providerParams: (testParams) => { return { params: { RoleArn: "arn:aws:iam::1234567890:role/Rigamarole", @@ -15,8 +15,9 @@ describe(fromTemporaryCredentials.name, () => { ...CTest.defaultRegionConfigProvider(testParams), }; }, - false - ); + profileCredentials: false, + fallbackRegion: "us-east-1", + }); ctest.testRegion(); diff --git a/packages/credential-providers/tests/fromTokenFile.integ.spec.ts b/packages/credential-providers/tests/fromTokenFile.integ.spec.ts index 0b70ae721d624..5768ed5794b83 100644 --- a/packages/credential-providers/tests/fromTokenFile.integ.spec.ts +++ b/packages/credential-providers/tests/fromTokenFile.integ.spec.ts @@ -1,6 +1,21 @@ import { fromTokenFile } from "@aws-sdk/credential-providers"; -import { describe, test as it } from "vitest"; +import { describe } from "vitest"; + +import { CTest } from "./_test-lib.spec"; describe(fromTokenFile.name, () => { - it("placeholder", () => {}); + const ctest = new CTest({ + credentialProvider: fromTokenFile, + providerParams: (testParams) => { + return { + webIdentityTokenFile: "token-filepath", + roleArn: "arn:aws:iam::1234567890:role/Rigamarole", + ...CTest.defaultRegionConfigProvider(testParams), + }; + }, + profileCredentials: false, + fallbackRegion: "us-east-1", + }); + + ctest.testRegion(); }); diff --git a/packages/credential-providers/tests/fromWebToken.integ.spec.ts b/packages/credential-providers/tests/fromWebToken.integ.spec.ts index b4fd1ffe8928a..ce07b04ab3db4 100644 --- a/packages/credential-providers/tests/fromWebToken.integ.spec.ts +++ b/packages/credential-providers/tests/fromWebToken.integ.spec.ts @@ -1,6 +1,21 @@ import { fromWebToken } from "@aws-sdk/credential-providers"; -import { describe, test as it } from "vitest"; +import { describe } from "vitest"; + +import { CTest } from "./_test-lib.spec"; describe(fromWebToken.name, () => { - it("placeholder", () => {}); + const ctest = new CTest({ + credentialProvider: fromWebToken, + providerParams: (testParams) => { + return { + webIdentityToken: "token-contents", + roleArn: "arn:aws:iam::1234567890:role/Rigamarole", + ...CTest.defaultRegionConfigProvider(testParams), + }; + }, + profileCredentials: false, + fallbackRegion: "us-east-1", + }); + + ctest.testRegion(); }); From 864834ce4a7e7be0d889da5825ba384268bc031c Mon Sep 17 00:00:00 2001 From: George Fu Date: Wed, 29 Oct 2025 11:56:11 -0400 Subject: [PATCH 7/7] test(credential-providers): more tests --- .../src/runtime/memoize-chain.spec.ts | 2 +- .../src/runtime/memoize-chain.ts | 5 + .../credential-provider-node.integ.spec.ts | 180 +----------------- packages/credential-providers/src/fromSSO.ts | 2 +- .../tests/{_test-lib.spec.ts => _test-lib.ts} | 17 +- .../tests/fromCognitoIdentity.integ.spec.ts | 39 +++- .../fromCognitoIdentityPool.integ.spec.ts | 39 +++- .../tests/fromContainerMetadata.integ.spec.ts | 39 +++- .../tests/fromEnv.integ.spec.ts | 38 +++- .../tests/fromHttp.integ.spec.ts | 58 +++++- .../tests/fromIni.integ.spec.ts | 85 ++++++++- .../tests/fromInstanceMetadata.integ.spec.ts | 37 +++- .../tests/fromNodeProviderChain.integ.spec.ts | 11 +- .../tests/fromProcess.integ.spec.ts | 83 +++++++- .../tests/fromSSO.integ.spec.ts | 65 ++++++- .../fromTemporaryCredentials.integ.spec.ts | 54 ++++-- .../tests/fromTokenFile.integ.spec.ts | 61 +++++- .../tests/fromWebToken.integ.spec.ts | 43 ++++- .../regionConfig/stsRegionDefaultResolver.ts | 12 ++ 19 files changed, 629 insertions(+), 241 deletions(-) rename packages/credential-providers/tests/{_test-lib.spec.ts => _test-lib.ts} (97%) diff --git a/packages/credential-provider-node/src/runtime/memoize-chain.spec.ts b/packages/credential-provider-node/src/runtime/memoize-chain.spec.ts index 7310491e27d86..1b089e40b3dd4 100644 --- a/packages/credential-provider-node/src/runtime/memoize-chain.spec.ts +++ b/packages/credential-provider-node/src/runtime/memoize-chain.spec.ts @@ -8,7 +8,7 @@ describe("memoize runtime config aware AWS credential chain", () => { let staticCredentials!: RuntimeConfigAwsCredentialIdentityProvider; let expiringCredentials!: RuntimeConfigAwsCredentialIdentityProvider; - const expiration = new Date(); + const expiration = new Date(Date.now() + 5_000); beforeEach(() => { vi.resetAllMocks(); diff --git a/packages/credential-provider-node/src/runtime/memoize-chain.ts b/packages/credential-provider-node/src/runtime/memoize-chain.ts index 2d9891a1a8956..9f518b7914709 100644 --- a/packages/credential-provider-node/src/runtime/memoize-chain.ts +++ b/packages/credential-provider-node/src/runtime/memoize-chain.ts @@ -34,6 +34,11 @@ export function memoizeChain( if (options?.forceRefresh) { return await chain(options); } + if (credentials?.expiration) { + if (credentials?.expiration?.getTime() < Date.now()) { + credentials = undefined; + } + } if (activeLock) { await activeLock; } else if (!credentials || treatAsExpired?.(credentials!)) { diff --git a/packages/credential-provider-node/tests/credential-provider-node.integ.spec.ts b/packages/credential-provider-node/tests/credential-provider-node.integ.spec.ts index af6eede709ace..02842b69c7e1b 100644 --- a/packages/credential-provider-node/tests/credential-provider-node.integ.spec.ts +++ b/packages/credential-provider-node/tests/credential-provider-node.integ.spec.ts @@ -4,11 +4,10 @@ import { fromCognitoIdentity, fromCognitoIdentityPool, fromIni, - fromNodeProviderChain, fromTokenFile, fromWebToken, } from "@aws-sdk/credential-providers"; -import { MockNodeHttpHandler, assumeRoleArns } from "@aws-sdk/credential-providers/tests/_test-lib.spec"; +import { assumeRoleArns, MockNodeHttpHandler } from "@aws-sdk/credential-providers/tests/_test-lib"; import { NodeHttpHandler } from "@smithy/node-http-handler"; import { externalDataInterceptor } from "@smithy/shared-ini-file-loader"; import type { HttpRequest, MiddlewareStack, ParsedIniData } from "@smithy/types"; @@ -19,7 +18,6 @@ import { homedir } from "node:os"; import { join } from "node:path"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test as it, vi } from "vitest"; -// eslint-disable-next-line no-restricted-imports import { defaultProvider } from "../src/defaultProvider"; describe("credential-provider-node integration test", () => { @@ -1510,181 +1508,5 @@ describe("credential-provider-node integration test", () => { }); }); }); - - describe("STS region logic", () => { - type Parameters = { - // has caller context client - withCaller: boolean; - // has region specified on the caller client - codeRegion: boolean; - // AWS_REGION is set - envRegion: boolean; - // profile regions are set - profileRegion: boolean; - // provider itself has a clientConfig.region - providerRegion: boolean; - // profile name - profile: string | undefined; - }; - - function serializeParams(params: Parameters) { - let buffer = ""; - for (const [key, value] of Object.entries(params)) { - if (typeof value === "boolean") { - if (value) { - buffer += ` ${key},`; - } - } else { - buffer += ` ${key} = ${value},`; - } - } - return buffer; - } - - for (const withCaller of [true, false]) { - for (const codeRegion of [true, false]) { - for (const envRegion of [true, false]) { - for (const profileRegion of [true, false]) { - for (const providerRegion of [true, false]) { - for (const profile of ["default", "alt", undefined]) { - if (!codeRegion && !profileRegion && !envRegion) { - continue; - } - - const params = { - withCaller, - codeRegion, - envRegion, - profileRegion, - providerRegion, - profile, - }; - - it(`${serializeParams(params)}`, async () => { - const region = await resolveStsRegion(params); - - if (providerRegion) { - expect(region).toBe("provider-region"); - return; - } - - if (profileRegion) { - expect(region).toBe(`${profile ?? "default"}-profile-region`); - return; - } - - if (codeRegion && withCaller) { - expect(region).toBe("code-region"); - return; - } - - if (envRegion) { - expect(region).toBe("env-region"); - return; - } - - expect(region).toBe("us-east-1"); - }); - } - } - } - } - } - } - - async function resolveStsRegion({ - withCaller, - envRegion, - profile, - profileRegion, - codeRegion, - providerRegion, - }: Parameters) { - if (envRegion) { - process.env.AWS_REGION = "env-region"; - } else { - delete process.env.AWS_REGION; - } - - if (profileRegion) { - iniProfileData = { - default: { - region: "default-profile-region", - role_arn: "ROLE_ARN", - role_session_name: "ROLE_SESSION_NAME", - external_id: "EXTERNAL_ID", - source_profile: "assume", - }, - assume: { - region: "assume-profile-region", - aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", - aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", - }, - alt: { - region: "alt-profile-region", - role_arn: "ROLE_ARN", - role_session_name: "ROLE_SESSION_NAME", - external_id: "EXTERNAL_ID", - source_profile: "assume2", - }, - assume2: { - region: "assume2-profile-region", - aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", - aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", - }, - }; - } else { - iniProfileData = { - default: { - role_arn: "ROLE_ARN", - role_session_name: "ROLE_SESSION_NAME", - external_id: "EXTERNAL_ID", - source_profile: "assume", - }, - assume: { - aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", - aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", - }, - alt: { - role_arn: "ROLE_ARN", - role_session_name: "ROLE_SESSION_NAME", - external_id: "EXTERNAL_ID", - source_profile: "assume2", - }, - assume2: { - aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", - aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", - }, - }; - } - setIniProfileData(iniProfileData); - - if (withCaller) { - const sts = new STS({ - profile, - region: codeRegion ? "code-region" : undefined, - credentials: fromNodeProviderChain({ - clientConfig: { - region: providerRegion ? "provider-region" : undefined, - }, - }), - }); - - await sts.getCallerIdentity({}); - const credentials = await sts.config.credentials(); - return credentials.sessionToken!.replace("STS_AR_SESSION_TOKEN_", ""); - } - - const provider = fromNodeProviderChain({ - profile, - clientConfig: { - region: providerRegion ? "provider-region" : undefined, - }, - }); - - const credentials = await provider(); - return credentials.sessionToken!.replace("STS_AR_SESSION_TOKEN_", ""); - } - }); }); }); diff --git a/packages/credential-providers/src/fromSSO.ts b/packages/credential-providers/src/fromSSO.ts index af04b52cc65c8..563de164873ab 100644 --- a/packages/credential-providers/src/fromSSO.ts +++ b/packages/credential-providers/src/fromSSO.ts @@ -44,6 +44,6 @@ import { AwsCredentialIdentityProvider } from "@smithy/types"; * * @public */ -export const fromSSO = (init: FromSSOInit = {}): AwsCredentialIdentityProvider => { +export const fromSSO = (init: Parameters[0] = {}): AwsCredentialIdentityProvider => { return _fromSSO({ ...init }); }; diff --git a/packages/credential-providers/tests/_test-lib.spec.ts b/packages/credential-providers/tests/_test-lib.ts similarity index 97% rename from packages/credential-providers/tests/_test-lib.spec.ts rename to packages/credential-providers/tests/_test-lib.ts index 946f26f97b68a..a3d8351a5a9bc 100644 --- a/packages/credential-providers/tests/_test-lib.spec.ts +++ b/packages/credential-providers/tests/_test-lib.ts @@ -1,4 +1,6 @@ import { S3 } from "@aws-sdk/client-s3"; +import { fromSSO } from "@aws-sdk/credential-providers"; +import { warning } from "@aws-sdk/region-config-resolver"; import { ParsedIniData, RuntimeConfigAwsCredentialIdentityProvider } from "@aws-sdk/types"; import { NodeHttpHandler } from "@smithy/node-http-handler"; import { HttpResponse } from "@smithy/protocol-http"; @@ -9,14 +11,10 @@ import { createHash } from "node:crypto"; import { homedir } from "node:os"; import { join } from "node:path"; import { PassThrough } from "node:stream"; -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test as it } from "vitest"; -import { fromSSO } from "@aws-sdk/credential-providers"; - -describe("placeholder for testing lib", () => { - it("", () => {}); -}); +import { afterAll, afterEach, beforeAll, beforeEach, expect, test as it } from "vitest"; export const assumeRoleArns: string[] = []; +warning.silence = true; let iniProfileData: ParsedIniData = null as any; export type CredentialTestParameters = { @@ -52,13 +50,13 @@ export class CTest

RuntimeConfigAwsCredentialIdentityP fallbackRegion, }: { credentialProvider: P; - providerParams: (testParams: CredentialTestParameters) => Parameters

[0]; + providerParams?: (testParams: CredentialTestParameters) => Parameters

[0]; profileCredentials?: boolean; filter?: (testParams: CredentialTestParameters) => boolean; fallbackRegion?: string; }) { this.credentialProvider = credentialProvider; - this.providerParams = providerParams; + this.providerParams = providerParams ?? CTest.defaultRegionConfigProvider; this.profileCredentials = !!profileCredentials; this.filter = filter ?? (() => true); this.fallbackRegion = fallbackRegion ?? "unresolved"; @@ -354,7 +352,7 @@ export class MockNodeHttpHandler { const region = (request.hostname.match(/(sts|cognito-identity|portal\.sso)\.(.*?)\./) || [, , "unknown"])[2]; - if (request.headers.Authorization === "container-authorization") { + if (request.headers.Authorization === "container-authorization" || request.hostname === "169.254.170.23") { body.write( JSON.stringify({ AccessKeyId: "CONTAINER_ACCESS_KEY", @@ -446,6 +444,7 @@ export class MockNodeHttpHandler { console.log(request); throw new Error("request not supported."); } + body.end(); return { response: new HttpResponse({ diff --git a/packages/credential-providers/tests/fromCognitoIdentity.integ.spec.ts b/packages/credential-providers/tests/fromCognitoIdentity.integ.spec.ts index 2cb1847cb8917..e84e0a6594ec6 100644 --- a/packages/credential-providers/tests/fromCognitoIdentity.integ.spec.ts +++ b/packages/credential-providers/tests/fromCognitoIdentity.integ.spec.ts @@ -1,7 +1,8 @@ +import { S3 } from "@aws-sdk/client-s3"; import { fromCognitoIdentity } from "@aws-sdk/credential-providers"; -import { describe } from "vitest"; +import { describe, expect, test as it } from "vitest"; -import { CTest } from "./_test-lib.spec"; +import { CTest } from "./_test-lib"; describe(fromCognitoIdentity.name, () => { const ctest = new CTest({ @@ -17,4 +18,38 @@ describe(fromCognitoIdentity.name, () => { }); ctest.testRegion(); + + describe("configure from env", () => { + it("is not configurable from env", async () => { + expect("ok").toBeTruthy(); + }); + }); + + describe("configure from profile", () => { + it("is not configurable from profile", async () => { + expect("ok").toBeTruthy(); + }); + }); + + describe("configure from code", () => { + it("should be configurable from code", async () => { + const s3 = new S3({ + region: "us-east-2", + credentials: fromCognitoIdentity({ + identityId: "us-east-2:128d0a74-c82f-4553-916d-90053example", + }), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_CODE: "e", + }, + accessKeyId: "COGNITO_ACCESS_KEY_ID", + expiration: new Date("3000-01-01T00:00:00.000Z"), + identityId: "us-east-2:128d0a74-c82f-4553-916d-90053example", + secretAccessKey: "COGNITO_SECRET_KEY", + sessionToken: "COGNITO_SESSION_TOKEN_us-east-2", + }); + }); + }); }); diff --git a/packages/credential-providers/tests/fromCognitoIdentityPool.integ.spec.ts b/packages/credential-providers/tests/fromCognitoIdentityPool.integ.spec.ts index 62f4c9dac2624..4b726d2e071dd 100644 --- a/packages/credential-providers/tests/fromCognitoIdentityPool.integ.spec.ts +++ b/packages/credential-providers/tests/fromCognitoIdentityPool.integ.spec.ts @@ -1,7 +1,8 @@ +import { S3 } from "@aws-sdk/client-s3"; import { fromCognitoIdentityPool } from "@aws-sdk/credential-providers"; -import { describe } from "vitest"; +import { describe, expect, test as it } from "vitest"; -import { CTest } from "./_test-lib.spec"; +import { CTest } from "./_test-lib"; describe(fromCognitoIdentityPool.name, () => { const ctest = new CTest({ @@ -17,4 +18,38 @@ describe(fromCognitoIdentityPool.name, () => { }); ctest.testRegion(); + + describe("configure from env", () => { + it("is not configurable from env", async () => { + expect("ok").toBeTruthy(); + }); + }); + + describe("configure from profile", () => { + it("is not configurable from profile", async () => { + expect("ok").toBeTruthy(); + }); + }); + + describe("configure from code", () => { + it("should be configurable from code", async () => { + const s3 = new S3({ + region: "us-east-2", + credentials: fromCognitoIdentityPool({ + identityPoolId: "us-east-2:COGNITO_IDENTITY_ID", + }), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_CODE: "e", + }, + accessKeyId: "COGNITO_ACCESS_KEY_ID", + expiration: new Date("3000-01-01T00:00:00.000Z"), + identityId: "us-east-2:COGNITO_IDENTITY_ID", + secretAccessKey: "COGNITO_SECRET_KEY", + sessionToken: "COGNITO_SESSION_TOKEN_us-east-2", + }); + }); + }); }); diff --git a/packages/credential-providers/tests/fromContainerMetadata.integ.spec.ts b/packages/credential-providers/tests/fromContainerMetadata.integ.spec.ts index 79a1fd998a8d4..4c79981f35b81 100644 --- a/packages/credential-providers/tests/fromContainerMetadata.integ.spec.ts +++ b/packages/credential-providers/tests/fromContainerMetadata.integ.spec.ts @@ -1,6 +1,41 @@ +import { S3 } from "@aws-sdk/client-s3"; import { fromContainerMetadata } from "@aws-sdk/credential-providers"; -import { describe, test as it } from "vitest"; +import { describe, expect, test as it } from "vitest"; + +import { CTest } from "./_test-lib"; describe(fromContainerMetadata.name, () => { - it("placeholder", () => {}); + const ctest = new CTest({ + credentialProvider: fromContainerMetadata, + providerParams: CTest.defaultRegionConfigProvider, + }); + + void S3; + void ctest; + + describe("configure from env", () => { + it.skip("should be configurable from env", async () => { + // todo: no integration hooks in this provider. + // process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI = "http://127.0.0.1"; + // process.env.AWS_CONTAINER_AUTHORIZATION_TOKEN = "container-authorization"; + // + // const s3 = new S3({ + // credentials: fromContainerMetadata({}), + // }); + // await s3.listBuckets(); + // expect(await s3.config.credentials()).toEqual({}); + }); + }); + + describe("configure from profile", () => { + it("is not configurable from profile", async () => { + expect("ok").toBeTruthy(); + }); + }); + + describe("configure from code", () => { + it("is not configurable form code", async () => { + expect("ok").toBeTruthy(); + }); + }); }); diff --git a/packages/credential-providers/tests/fromEnv.integ.spec.ts b/packages/credential-providers/tests/fromEnv.integ.spec.ts index 2d2b627b5a1e8..ea930ea4fdd50 100644 --- a/packages/credential-providers/tests/fromEnv.integ.spec.ts +++ b/packages/credential-providers/tests/fromEnv.integ.spec.ts @@ -1,15 +1,41 @@ +import { S3 } from "@aws-sdk/client-s3"; import { fromEnv } from "@aws-sdk/credential-providers"; -import { describe, test as it } from "vitest"; +import { describe, expect, test as it } from "vitest"; -import { CTest } from "./_test-lib.spec"; +import { CTest } from "./_test-lib"; describe(fromEnv.name, () => { const ctest = new CTest({ credentialProvider: fromEnv, - providerParams: () => { - return {}; - }, }); - it("placeholder", () => {}); + describe("configure from env", () => { + it("is configurable from env", async () => { + process.env.AWS_ACCESS_KEY_ID = "AK"; + process.env.AWS_SECRET_ACCESS_KEY = "SK"; + process.env.AWS_SESSION_TOKEN = "session-token-env"; + const s3 = new S3({}); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_ENV_VARS: "g", + }, + accessKeyId: "AK", + secretAccessKey: "SK", + sessionToken: "session-token-env", + }); + }); + }); + + describe("configure from profile", () => { + it("is not configurable from profile", async () => { + expect("ok").toBeTruthy(); + }); + }); + + describe("configure from code", () => { + it("is not configurable from code", async () => { + expect("ok").toBeTruthy(); + }); + }); }); diff --git a/packages/credential-providers/tests/fromHttp.integ.spec.ts b/packages/credential-providers/tests/fromHttp.integ.spec.ts index 3638276546b68..4af5de2221dd9 100644 --- a/packages/credential-providers/tests/fromHttp.integ.spec.ts +++ b/packages/credential-providers/tests/fromHttp.integ.spec.ts @@ -1,6 +1,60 @@ +import { S3 } from "@aws-sdk/client-s3"; import { fromHttp } from "@aws-sdk/credential-providers"; -import { describe, test as it } from "vitest"; +import { describe, expect, test as it } from "vitest"; + +import { CTest } from "./_test-lib"; describe(fromHttp.name, () => { - it("placeholder", () => {}); + const ctest = new CTest({ + credentialProvider: fromHttp, + }); + + describe("configure from env", () => { + it("is not configurable from env", async () => { + process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI = "http://169.254.170.23"; + process.env.AWS_CONTAINER_AUTHORIZATION_TOKEN = "container-authorization"; + const s3 = new S3({ + credentials: fromHttp(), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_HTTP: "z", + CREDENTIALS_CODE: "e", + }, + accessKeyId: "CONTAINER_ACCESS_KEY", + expiration: new Date("3000-01-01T00:00:00.000Z"), + secretAccessKey: "CONTAINER_SECRET_ACCESS_KEY", + sessionToken: "CONTAINER_TOKEN", + }); + }); + }); + + describe("configure from profile", () => { + it("is not configurable from profile", async () => { + expect("ok").toBeTruthy(); + }); + }); + + describe("configure from code", () => { + it("should be configurable from code", async () => { + const s3 = new S3({ + credentials: fromHttp({ + awsContainerCredentialsFullUri: "http://169.254.170.23", + authorizationToken: "container-authorization", + }), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_HTTP: "z", + CREDENTIALS_CODE: "e", + }, + accessKeyId: "CONTAINER_ACCESS_KEY", + expiration: new Date("3000-01-01T00:00:00.000Z"), + secretAccessKey: "CONTAINER_SECRET_ACCESS_KEY", + sessionToken: "CONTAINER_TOKEN", + }); + }); + }); }); diff --git a/packages/credential-providers/tests/fromIni.integ.spec.ts b/packages/credential-providers/tests/fromIni.integ.spec.ts index b465422c00dbd..ee8c500c6c780 100644 --- a/packages/credential-providers/tests/fromIni.integ.spec.ts +++ b/packages/credential-providers/tests/fromIni.integ.spec.ts @@ -1,7 +1,8 @@ +import { S3 } from "@aws-sdk/client-s3"; import { fromIni } from "@aws-sdk/credential-providers"; -import { describe } from "vitest"; +import { describe, expect, test as it } from "vitest"; -import { CTest } from "./_test-lib.spec"; +import { CTest } from "./_test-lib"; describe(fromIni.name, () => { const ctest = new CTest({ @@ -12,4 +13,84 @@ describe(fromIni.name, () => { }); ctest.testRegion(); + + describe("configure from env", () => { + it("is configurable from env", async () => { + process.env.AWS_PROFILE = "alt"; + ctest.setIni({ + alt: { + region: "us-west-2", + aws_access_key_id: "A", + aws_secret_access_key: "S", + aws_session_token: "T", + }, + }); + const s3 = new S3({ + credentials: fromIni(), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_CODE: "e", + CREDENTIALS_PROFILE: "n", + }, + accessKeyId: "A", + secretAccessKey: "S", + sessionToken: "T", + }); + }); + }); + + describe("configure from profile", () => { + it("is not configurable from profile", async () => { + ctest.setIni({ + default: { + region: "us-west-2", + aws_access_key_id: "A", + aws_secret_access_key: "S", + aws_session_token: "T", + }, + }); + const s3 = new S3({ + credentials: fromIni(), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_CODE: "e", + CREDENTIALS_PROFILE: "n", + }, + accessKeyId: "A", + secretAccessKey: "S", + sessionToken: "T", + }); + }); + }); + + describe("configure from code", () => { + it("should be configurable from code", async () => { + ctest.setIni({ + alt: { + region: "us-west-2", + aws_access_key_id: "A", + aws_secret_access_key: "S", + aws_session_token: "T", + }, + }); + const s3 = new S3({ + profile: "alt", + credentials: fromIni(), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_CODE: "e", + CREDENTIALS_PROFILE: "n", + }, + accessKeyId: "A", + secretAccessKey: "S", + sessionToken: "T", + }); + }); + }); }); diff --git a/packages/credential-providers/tests/fromInstanceMetadata.integ.spec.ts b/packages/credential-providers/tests/fromInstanceMetadata.integ.spec.ts index 83bedb05f878e..3514a4963c746 100644 --- a/packages/credential-providers/tests/fromInstanceMetadata.integ.spec.ts +++ b/packages/credential-providers/tests/fromInstanceMetadata.integ.spec.ts @@ -1,6 +1,39 @@ +import { S3 } from "@aws-sdk/client-s3"; import { fromInstanceMetadata } from "@aws-sdk/credential-providers"; -import { describe, test as it } from "vitest"; +import { describe, expect, test as it } from "vitest"; + +import { CTest } from "./_test-lib"; describe(fromInstanceMetadata.name, () => { - it("placeholder", () => {}); + const ctest = new CTest({ + credentialProvider: fromInstanceMetadata, + }); + + void ctest; + + describe("configure from env", () => { + it.skip("is configurable from env", async () => { + void S3; + // todo: there are no integration hooks in this provider. + // process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI = "http://169.254.170.23"; + // process.env.AWS_CONTAINER_AUTHORIZATION_TOKEN = "container-authorization"; + // const s3 = new S3({ + // credentials: fromInstanceMetadata(), + // }); + // await s3.listBuckets(); + // expect(await s3.config.credentials()).toEqual({}); + }); + }); + + describe("configure from profile", () => { + it("is not configurable from profile", async () => { + expect("ok").toBeTruthy(); + }); + }); + + describe("configure from code", () => { + it("is not configurable from code", async () => { + expect("ok").toBeTruthy(); + }); + }); }); diff --git a/packages/credential-providers/tests/fromNodeProviderChain.integ.spec.ts b/packages/credential-providers/tests/fromNodeProviderChain.integ.spec.ts index 7c2f70b4500b2..0453ea427a54c 100644 --- a/packages/credential-providers/tests/fromNodeProviderChain.integ.spec.ts +++ b/packages/credential-providers/tests/fromNodeProviderChain.integ.spec.ts @@ -1,7 +1,8 @@ +import { S3 } from "@aws-sdk/client-s3"; import { fromNodeProviderChain } from "@aws-sdk/credential-providers"; -import { describe } from "vitest"; +import { describe, expect, test as it } from "vitest"; -import { CTest } from "./_test-lib.spec"; +import { CTest } from "./_test-lib"; describe(fromNodeProviderChain.name, () => { const ctest = new CTest({ @@ -12,4 +13,10 @@ describe(fromNodeProviderChain.name, () => { }); ctest.testRegion(); + + void S3; + + it("is tested in the credential-provider-node.integ.spec.ts file", async () => { + expect("ok").toBeTruthy(); + }); }); diff --git a/packages/credential-providers/tests/fromProcess.integ.spec.ts b/packages/credential-providers/tests/fromProcess.integ.spec.ts index 9be28a562b493..eca7a513f362e 100644 --- a/packages/credential-providers/tests/fromProcess.integ.spec.ts +++ b/packages/credential-providers/tests/fromProcess.integ.spec.ts @@ -1,6 +1,85 @@ +import { S3 } from "@aws-sdk/client-s3"; import { fromProcess } from "@aws-sdk/credential-providers"; -import { describe, test as it } from "vitest"; +import { describe, expect, test as it } from "vitest"; + +import { CTest } from "./_test-lib"; describe(fromProcess.name, () => { - it("placeholder", () => {}); + const ctest = new CTest({ + credentialProvider: fromProcess, + }); + + describe("configure from env", () => { + it("is partially configurable from env - but only in selecting a profile", async () => { + process.env.AWS_PROFILE = "alt"; + ctest.setIni({ + alt: { + credential_process: "credential-process", + }, + }); + const s3 = new S3({ + region: "us-east-2", + credentials: fromProcess({}), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_CODE: "e", + CREDENTIALS_PROCESS: "w", + }, + accessKeyId: "PROCESS_ACCESS_KEY_ID", + secretAccessKey: "PROCESS_SECRET_ACCESS_KEY", + sessionToken: "PROCESS_SESSION_TOKEN", + }); + }); + }); + + describe("configure from profile", () => { + it("is configurable from profile", async () => { + ctest.setIni({ + default: { + credential_process: "credential-process", + }, + }); + const s3 = new S3({ + region: "us-east-2", + credentials: fromProcess(), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_CODE: "e", + CREDENTIALS_PROCESS: "w", + }, + accessKeyId: "PROCESS_ACCESS_KEY_ID", + secretAccessKey: "PROCESS_SECRET_ACCESS_KEY", + sessionToken: "PROCESS_SESSION_TOKEN", + }); + }); + }); + + describe("configure from code", () => { + it("is partially configurable from code - but only in selecting a profile", async () => { + ctest.setIni({ + alt: { + credential_process: "credential-process", + }, + }); + const s3 = new S3({ + region: "us-east-2", + profile: "alt", + credentials: fromProcess({}), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_CODE: "e", + CREDENTIALS_PROCESS: "w", + }, + accessKeyId: "PROCESS_ACCESS_KEY_ID", + secretAccessKey: "PROCESS_SECRET_ACCESS_KEY", + sessionToken: "PROCESS_SESSION_TOKEN", + }); + }); + }); }); diff --git a/packages/credential-providers/tests/fromSSO.integ.spec.ts b/packages/credential-providers/tests/fromSSO.integ.spec.ts index 4057e4c9f638e..5144a13341d00 100644 --- a/packages/credential-providers/tests/fromSSO.integ.spec.ts +++ b/packages/credential-providers/tests/fromSSO.integ.spec.ts @@ -1,7 +1,8 @@ +import { S3 } from "@aws-sdk/client-s3"; import { fromSSO } from "@aws-sdk/credential-providers"; -import { describe } from "vitest"; +import { describe, expect, test as it } from "vitest"; -import { CTest } from "./_test-lib.spec"; +import { CTest } from "./_test-lib"; describe(fromSSO.name, () => { const ctest = new CTest({ @@ -11,7 +12,7 @@ describe(fromSSO.name, () => { ssoStartUrl: "SSO_START_URL", ssoAccountId: "1234567890", ssoRegion: "sso-region-1", - ssoRoleName: "arn:aws:iam::1234567890:role/Rigamarole", + ssoRoleName: "arn:aws:iam::1234567890:role/Rigmarole", ...CTest.defaultRegionConfigProvider(testParams), }; }, @@ -20,4 +21,62 @@ describe(fromSSO.name, () => { }); ctest.testRegion(); + + describe("configure from env", () => { + it("is not configurable from env", async () => { + expect("ok").toBeTruthy(); + }); + }); + + describe("configure from profile", () => { + it("is not configurable from profile", async () => { + ctest.setIni({ + default: { + sso_start_url: "SSO_START_URL", + sso_account_id: "1234567890", + sso_region: "us-east-1", + sso_role_name: "Rigmarole", + }, + }); + const s3 = new S3({ + region: "us-east-1", + credentials: fromSSO({}), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_CODE: "e", + CREDENTIALS_SSO_LEGACY: "u", + }, + accessKeyId: "SSO_ACCESS_KEY_ID", + expiration: new Date("3000-01-01T00:00:00.000Z"), + secretAccessKey: "SSO_SECRET_ACCESS_KEY", + sessionToken: "SSO_SESSION_TOKEN_us-east-1", + }); + }); + }); + + describe("configure from code", () => { + it("should be configurable from code", async () => { + const s3 = new S3({ + credentials: fromSSO({ + ssoStartUrl: "SSO_START_URL", + ssoAccountId: "1234567890", + ssoRegion: "us-east-1", + ssoRoleName: "Rigmarole", + }), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_CODE: "e", + CREDENTIALS_SSO_LEGACY: "u", + }, + accessKeyId: "SSO_ACCESS_KEY_ID", + expiration: new Date("3000-01-01T00:00:00.000Z"), + secretAccessKey: "SSO_SECRET_ACCESS_KEY", + sessionToken: "SSO_SESSION_TOKEN_us-east-1", + }); + }); + }); }); diff --git a/packages/credential-providers/tests/fromTemporaryCredentials.integ.spec.ts b/packages/credential-providers/tests/fromTemporaryCredentials.integ.spec.ts index 605ccfc2fc783..a961c959b38c3 100644 --- a/packages/credential-providers/tests/fromTemporaryCredentials.integ.spec.ts +++ b/packages/credential-providers/tests/fromTemporaryCredentials.integ.spec.ts @@ -2,7 +2,7 @@ import { S3 } from "@aws-sdk/client-s3"; import { fromTemporaryCredentials } from "@aws-sdk/credential-providers"; import { describe, expect, test as it } from "vitest"; -import { CTest } from "./_test-lib.spec"; +import { CTest } from "./_test-lib"; describe(fromTemporaryCredentials.name, () => { const ctest = new CTest({ @@ -10,7 +10,7 @@ describe(fromTemporaryCredentials.name, () => { providerParams: (testParams) => { return { params: { - RoleArn: "arn:aws:iam::1234567890:role/Rigamarole", + RoleArn: "arn:aws:iam::1234567890:role/Rigmarole", }, ...CTest.defaultRegionConfigProvider(testParams), }; @@ -21,28 +21,42 @@ describe(fromTemporaryCredentials.name, () => { ctest.testRegion(); - it("should resolve region", async () => { - ctest.setIni({ - alt: { - region: "us-east-2", - }, + describe("configure from env", () => { + it("is not configurable from env", async () => { + expect("ok").toBeTruthy(); }); + }); - const s3 = new S3({ - profile: "alt", - credentials: fromTemporaryCredentials({ - masterCredentials: { - accessKeyId: "M", - secretAccessKey: "M", - }, - params: { - RoleArn: "arn:aws:iam::1234567890:role/Rigamarole", - }, - }), + describe("configure from profile", () => { + it("is not configurable from profile", async () => { + expect("ok").toBeTruthy(); }); + }); + + describe("configure from code", () => { + it("should be configurable from code", async () => { + ctest.setIni({ + alt: { + region: "us-east-2", + }, + }); + + const s3 = new S3({ + profile: "alt", + credentials: fromTemporaryCredentials({ + masterCredentials: { + accessKeyId: "M", + secretAccessKey: "M", + }, + params: { + RoleArn: "arn:aws:iam::1234567890:role/Rigmarole", + }, + }), + }); - expect(await s3.config.credentials()).toMatchObject({ - sessionToken: "STS_AR_SESSION_TOKEN_us-east-2", + expect(await s3.config.credentials()).toMatchObject({ + sessionToken: "STS_AR_SESSION_TOKEN_us-east-2", + }); }); }); }); diff --git a/packages/credential-providers/tests/fromTokenFile.integ.spec.ts b/packages/credential-providers/tests/fromTokenFile.integ.spec.ts index 5768ed5794b83..e53b7f8678826 100644 --- a/packages/credential-providers/tests/fromTokenFile.integ.spec.ts +++ b/packages/credential-providers/tests/fromTokenFile.integ.spec.ts @@ -1,7 +1,8 @@ +import { S3 } from "@aws-sdk/client-s3"; import { fromTokenFile } from "@aws-sdk/credential-providers"; -import { describe } from "vitest"; +import { describe, expect, test as it } from "vitest"; -import { CTest } from "./_test-lib.spec"; +import { CTest } from "./_test-lib"; describe(fromTokenFile.name, () => { const ctest = new CTest({ @@ -9,7 +10,7 @@ describe(fromTokenFile.name, () => { providerParams: (testParams) => { return { webIdentityTokenFile: "token-filepath", - roleArn: "arn:aws:iam::1234567890:role/Rigamarole", + roleArn: "arn:aws:iam::1234567890:role/Rigmarole", ...CTest.defaultRegionConfigProvider(testParams), }; }, @@ -18,4 +19,58 @@ describe(fromTokenFile.name, () => { }); ctest.testRegion(); + + describe("configure from env", () => { + it("is configurable from env", async () => { + process.env.AWS_WEB_IDENTITY_TOKEN_FILE = "token-filepath"; + process.env.AWS_ROLE_ARN = "arn:aws:iam::1234567890:role/Rigmarole"; + process.env.AWS_ROLE_SESSION_NAME = "role-session-1234"; + const s3 = new S3({ + region: "us-west-2", + credentials: fromTokenFile({}), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_CODE: "e", + CREDENTIALS_STS_ASSUME_ROLE_WEB_ID: "k", + CREDENTIALS_ENV_VARS_STS_WEB_ID_TOKEN: "h", + }, + accessKeyId: "STS_ARWI_ACCESS_KEY_ID", + expiration: new Date("3000-01-01T00:00:00.000Z"), + secretAccessKey: "STS_ARWI_SECRET_ACCESS_KEY", + sessionToken: "STS_ARWI_SESSION_TOKEN_us-west-2", + }); + }); + }); + + describe("configure from profile", () => { + it("is not configurable from profile", async () => { + expect("ok").toBeTruthy(); + }); + }); + + describe("configure from code", () => { + it("should be configurable from code", async () => { + const s3 = new S3({ + region: "us-west-2", + credentials: fromTokenFile({ + webIdentityTokenFile: "token-filepath", + roleArn: "arn:aws:iam::1234567890:role/Rigmarole", + roleSessionName: "role-session-1234", + }), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_CODE: "e", + CREDENTIALS_STS_ASSUME_ROLE_WEB_ID: "k", + }, + accessKeyId: "STS_ARWI_ACCESS_KEY_ID", + expiration: new Date("3000-01-01T00:00:00.000Z"), + secretAccessKey: "STS_ARWI_SECRET_ACCESS_KEY", + sessionToken: "STS_ARWI_SESSION_TOKEN_us-west-2", + }); + }); + }); }); diff --git a/packages/credential-providers/tests/fromWebToken.integ.spec.ts b/packages/credential-providers/tests/fromWebToken.integ.spec.ts index ce07b04ab3db4..a19d7516324b5 100644 --- a/packages/credential-providers/tests/fromWebToken.integ.spec.ts +++ b/packages/credential-providers/tests/fromWebToken.integ.spec.ts @@ -1,7 +1,8 @@ +import { S3 } from "@aws-sdk/client-s3"; import { fromWebToken } from "@aws-sdk/credential-providers"; -import { describe } from "vitest"; +import { describe, expect, test as it } from "vitest"; -import { CTest } from "./_test-lib.spec"; +import { CTest } from "./_test-lib"; describe(fromWebToken.name, () => { const ctest = new CTest({ @@ -9,7 +10,7 @@ describe(fromWebToken.name, () => { providerParams: (testParams) => { return { webIdentityToken: "token-contents", - roleArn: "arn:aws:iam::1234567890:role/Rigamarole", + roleArn: "arn:aws:iam::1234567890:role/Rigmarole", ...CTest.defaultRegionConfigProvider(testParams), }; }, @@ -18,4 +19,40 @@ describe(fromWebToken.name, () => { }); ctest.testRegion(); + + describe("configure from env", () => { + it("is not configurable from env", async () => { + expect("ok").toBeTruthy(); + }); + }); + + describe("configure from profile", () => { + it("is not configurable from profile", async () => { + expect("ok").toBeTruthy(); + }); + }); + + describe("configure from code", () => { + it("should be configurable from code", async () => { + const s3 = new S3({ + region: "us-west-2", + credentials: fromWebToken({ + webIdentityToken: "token-contents", + roleArn: "arn:aws:iam::1234567890:role/Rigmarole", + roleSessionName: "role-session-1234", + }), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_CODE: "e", + CREDENTIALS_STS_ASSUME_ROLE_WEB_ID: "k", + }, + accessKeyId: "STS_ARWI_ACCESS_KEY_ID", + expiration: new Date("3000-01-01T00:00:00.000Z"), + secretAccessKey: "STS_ARWI_SECRET_ACCESS_KEY", + sessionToken: "STS_ARWI_SESSION_TOKEN_us-west-2", + }); + }); + }); }); diff --git a/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.ts b/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.ts index c66fa8bf14fab..52fb249f6ce90 100644 --- a/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.ts +++ b/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.ts @@ -12,9 +12,21 @@ export function stsRegionDefaultResolver(loaderConfig: LocalConfigOptions = {}) { ...NODE_REGION_CONFIG_OPTIONS, async default() { + if (!warning.silence) { + console.warn( + "@aws-sdk - WARN - default STS region of us-east-1 used. See @aws-sdk/credential-providers README and set a region explicitly." + ); + } return "us-east-1"; }, }, { ...NODE_REGION_CONFIG_FILE_OPTIONS, ...loaderConfig } ); } + +/** + * @internal + */ +export const warning = { + silence: false, +};