envFn;
@@ -48,6 +49,7 @@ public final class ChainSetup {
private ChainSetup(Builder builder) {
this.executor = builder.executor;
this.profileNameOverride = builder.profileNameOverride;
+ this.regionOverride = builder.regionOverride;
this.properties = Context.create();
this.envFn = builder.envFn;
this.profileFile = builder.profileFile;
@@ -82,6 +84,19 @@ public String profileNameOverride() {
return profileNameOverride;
}
+ /**
+ * Returns the client-specified region override used by providers that resolve credentials via a service call
+ * (e.g., STS, SSO), or {@code null} to fall back to the {@code AWS_REGION}/{@code AWS_DEFAULT_REGION}
+ * environment variables and the profile {@code region} property.
+ *
+ * This affects only which regional endpoint those providers call; it does not parameterize the chain itself.
+ *
+ * @return the region override, or {@code null}.
+ */
+ public String regionOverride() {
+ return regionOverride;
+ }
+
/**
* Returns the value of the given environment variable, or {@code null} if not set.
*
@@ -214,6 +229,7 @@ public record NamedResolver(String name, Set featureIds, Id
public static final class Builder {
private ScheduledExecutorService executor;
private String profileNameOverride;
+ private String regionOverride;
private Function envFn = System::getenv;
private AwsProfileFile profileFile;
@@ -242,6 +258,19 @@ public Builder profileNameOverride(String profileNameOverride) {
return this;
}
+ /**
+ * Sets the region override. When set, providers that resolve credentials via a service call (e.g., STS,
+ * SSO) use this region for their endpoint instead of resolving it from {@code AWS_REGION},
+ * {@code AWS_DEFAULT_REGION}, or the profile {@code region} property.
+ *
+ * @param regionOverride the region to use, e.g. {@code "us-west-2"}.
+ * @return this builder.
+ */
+ public Builder regionOverride(String regionOverride) {
+ this.regionOverride = regionOverride;
+ return this;
+ }
+
/**
* Sets the function used to resolve environment variables. Defaults to
* {@link System#getenv(String)}. Override in tests for isolation.
diff --git a/aws/aws-credential-chain/src/main/java/software/amazon/smithy/java/aws/credentials/chain/CredentialChain.java b/aws/aws-credential-chain/src/main/java/software/amazon/smithy/java/aws/credentials/chain/IdentityChain.java
similarity index 78%
rename from aws/aws-credential-chain/src/main/java/software/amazon/smithy/java/aws/credentials/chain/CredentialChain.java
rename to aws/aws-credential-chain/src/main/java/software/amazon/smithy/java/aws/credentials/chain/IdentityChain.java
index 6a24c50f28..0426a4097a 100644
--- a/aws/aws-credential-chain/src/main/java/software/amazon/smithy/java/aws/credentials/chain/CredentialChain.java
+++ b/aws/aws-credential-chain/src/main/java/software/amazon/smithy/java/aws/credentials/chain/IdentityChain.java
@@ -24,31 +24,32 @@
import software.amazon.smithy.java.logging.InternalLogger;
/**
- * A credential provider chain.
+ * A chain of identity providers, parameterized by the {@link Identity} type it resolves (e.g.,
+ * {@code AwsCredentialsIdentity} or {@code TokenIdentity}).
*
* Discovers {@link ChainIdentityProvider} implementations via {@link ServiceLoader}, assembles them into an
- * ordered chain based on {@link StandardProvider} slots and relative ordering constraints, and resolves
- * credentials by trying each provider in order.
+ * ordered chain based on {@link StandardProvider} slots and relative ordering constraints, and resolves an
+ * identity by trying each provider in order.
*
*
Usage:
- * {@snippet lang="java" :
- * var chain = CredentialChain.create();
+ * {@snippet lang = "java":
+ * var chain = IdentityChain.create();
* var result = chain.resolveIdentity(Context.empty());
- * }
+ *}
*
*
The chain is assembled once at creation time. Providers that are not on the classpath simply don't
- * participate: their slots are skipped. If no provider in the chain can resolve credentials, the chain returns an
+ * participate: their slots are skipped. If no provider in the chain can resolve an identity, the chain returns an
* error result describing which providers were tried.
*/
-public final class CredentialChain implements IdentityResolver, AutoCloseable {
+public final class IdentityChain implements IdentityResolver, AutoCloseable {
- private static final InternalLogger LOGGER = InternalLogger.getLogger(CredentialChain.class);
+ private static final InternalLogger LOGGER = InternalLogger.getLogger(IdentityChain.class);
private final Class identityType;
private final List resolvers;
private final ScheduledExecutorService executor;
- private CredentialChain(
+ private IdentityChain(
Class identityType,
List resolvers,
ScheduledExecutorService executor
@@ -59,60 +60,92 @@ private CredentialChain(
}
/**
- * Create a credential chain by discovering providers via ServiceLoader.
+ * Create an identity chain by discovering providers via ServiceLoader.
*
* @param identityType Identity type to resolve.
* @return the assembled chain.
* @throws IllegalStateException if two providers claim the same standard slot.
*/
- public static CredentialChain create(Class identityType) {
- return create(identityType, Executors.newSingleThreadScheduledExecutor(r2 -> {
+ public static IdentityChain create(Class identityType) {
+ return create(identityType, defaultExecutor(), null, null);
+ }
+
+ /**
+ * Create an identity chain by discovering providers via ServiceLoader, using a caller-supplied AWS
+ * config/credentials file and region, with a default background-refresh executor.
+ *
+ * @param identityType Identity type to resolve.
+ * @param profileFile Already-parsed profile file to use, or {@code null} to load from the default locations.
+ * @param regionOverride Region for service-calling providers to use, or {@code null} to resolve it normally.
+ * @return the assembled chain.
+ * @throws IllegalStateException if two providers claim the same standard slot.
+ */
+ public static IdentityChain create(
+ Class identityType,
+ AwsProfileFile profileFile,
+ String regionOverride
+ ) {
+ return create(identityType, defaultExecutor(), profileFile, regionOverride);
+ }
+
+ private static ScheduledExecutorService defaultExecutor() {
+ return Executors.newSingleThreadScheduledExecutor(r2 -> {
Thread t = new Thread(r2, "aws-credential-chain-refresh");
t.setDaemon(true);
return t;
- }));
+ });
}
/**
- * Create a credential chain by discovering providers via ServiceLoader.
+ * Create an identity chain by discovering providers via ServiceLoader.
*
* @param identityType Identity type to resolve.
* @param ex Executor used for background resolution.
* @return the assembled chain.
* @throws IllegalStateException if two providers claim the same standard slot.
*/
- public static CredentialChain create(Class identityType, ScheduledExecutorService ex) {
- return create(identityType, ex, null);
+ public static IdentityChain create(Class identityType, ScheduledExecutorService ex) {
+ return create(identityType, ex, null, null);
}
/**
- * Create a credential chain by discovering providers via ServiceLoader, using a caller-supplied AWS
- * config/credentials file.
+ * Create an identity chain by discovering providers via ServiceLoader, using a caller-supplied AWS
+ * config/credentials file and region.
*
* When {@code profileFile} is non-null, the {@code SHARED_CONFIG} provider uses it instead of reading
* {@code ~/.aws/config} and {@code ~/.aws/credentials} from disk. Use this when the file has already been
* loaded, or to point the chain at a non-default location.
*
+ *
When {@code regionOverride} is non-null, providers that resolve credentials via a service call (e.g.,
+ * STS, SSO) use it for their endpoint instead of resolving the region from the environment or profile. This is
+ * how a client's configured region flows into credential resolution.
+ *
* @param identityType Identity type to resolve.
* @param ex Executor used for background resolution.
* @param profileFile Already-parsed profile file to use, or {@code null} to load from the default locations.
+ * @param regionOverride Region for service-calling providers to use, or {@code null} to resolve it normally.
* @return the assembled chain.
* @throws IllegalStateException if two providers claim the same standard slot.
*/
- public static CredentialChain create(
+ public static IdentityChain create(
Class identityType,
ScheduledExecutorService ex,
- AwsProfileFile profileFile
+ AwsProfileFile profileFile,
+ String regionOverride
) {
List registrations = new ArrayList<>();
for (ChainIdentityProvider r : ServiceLoader.load(ChainIdentityProvider.class)) {
registrations.add(r);
}
- ChainSetup setup = ChainSetup.builder().executor(ex).profileFile(profileFile).build();
+ ChainSetup setup = ChainSetup.builder()
+ .executor(ex)
+ .profileFile(profileFile)
+ .regionOverride(regionOverride)
+ .build();
return assemble(identityType, registrations, ex, setup);
}
- static CredentialChain assemble(
+ static IdentityChain assemble(
Class identityType,
List registrations,
ScheduledExecutorService executor
@@ -124,7 +157,7 @@ static CredentialChain assemble(
* Assemble a chain using a caller-supplied {@link ChainSetup}. Lets tests inject a deterministic environment
* and profile rather than reading the real process environment and config files.
*/
- static CredentialChain assemble(
+ static IdentityChain assemble(
Class identityType,
List registrations,
ScheduledExecutorService executor,
@@ -153,7 +186,7 @@ static CredentialChain assemble(
var ordered = setup.resolvers();
if (LOGGER.isDebugEnabled()) {
- LOGGER.debug("Assembled credential chain: {}",
+ LOGGER.debug("Assembled identity chain: {}",
ordered.stream().map(ChainSetup.NamedResolver::name).collect(Collectors.joining(", ")));
}
@@ -168,7 +201,7 @@ static CredentialChain assemble(
}
}
warnDetectedButUnclaimed(claimed);
- return new CredentialChain<>(identityType, Collections.unmodifiableList(ordered), executor);
+ return new IdentityChain<>(identityType, Collections.unmodifiableList(ordered), executor);
}
private static List sortByOrdering(List providers) {
diff --git a/aws/aws-credential-chain/src/main/java/software/amazon/smithy/java/aws/credentials/chain/OrderingConstraint.java b/aws/aws-credential-chain/src/main/java/software/amazon/smithy/java/aws/credentials/chain/OrderingConstraint.java
index 284fbdfbf2..c990f2cb17 100644
--- a/aws/aws-credential-chain/src/main/java/software/amazon/smithy/java/aws/credentials/chain/OrderingConstraint.java
+++ b/aws/aws-credential-chain/src/main/java/software/amazon/smithy/java/aws/credentials/chain/OrderingConstraint.java
@@ -6,7 +6,7 @@
package software.amazon.smithy.java.aws.credentials.chain;
/**
- * Describes where a {@link ChainIdentityProvider} sits in the credential chain.
+ * Describes where a {@link ChainIdentityProvider} sits in the {@link IdentityChain}.
*
* Three forms:
*
diff --git a/aws/aws-credential-chain/src/test/java/software/amazon/smithy/java/aws/credentials/chain/FeatureIdTest.java b/aws/aws-credential-chain/src/test/java/software/amazon/smithy/java/aws/credentials/chain/FeatureIdTest.java
index 4aa61d701e..6ed3d7b1fa 100644
--- a/aws/aws-credential-chain/src/test/java/software/amazon/smithy/java/aws/credentials/chain/FeatureIdTest.java
+++ b/aws/aws-credential-chain/src/test/java/software/amazon/smithy/java/aws/credentials/chain/FeatureIdTest.java
@@ -24,7 +24,7 @@ class FeatureIdTest {
@Test
void successfulProviderEmitsFeatureId() {
- var chain = CredentialChain.assemble(AwsCredentialsIdentity.class,
+ var chain = IdentityChain.assemble(AwsCredentialsIdentity.class,
List.of(
provider("env",
StandardProvider.ENVIRONMENT,
@@ -44,7 +44,7 @@ void successfulProviderEmitsFeatureId() {
@Test
void failedProviderDoesNotEmitFeatureId() {
- var chain = CredentialChain.assemble(AwsCredentialsIdentity.class,
+ var chain = IdentityChain.assemble(AwsCredentialsIdentity.class,
List.of(
provider("env",
StandardProvider.ENVIRONMENT,
@@ -68,7 +68,7 @@ void failedProviderDoesNotEmitFeatureId() {
@Test
void multipleFeatureIdsEmitted() {
- var chain = CredentialChain.assemble(AwsCredentialsIdentity.class,
+ var chain = IdentityChain.assemble(AwsCredentialsIdentity.class,
List.of(
provider("proc",
StandardProvider.SHARED_CONFIG,
@@ -93,7 +93,7 @@ void multipleFeatureIdsEmitted() {
@Test
void noFeatureIdsWhenContextKeyNotSet() {
- var chain = CredentialChain.assemble(AwsCredentialsIdentity.class,
+ var chain = IdentityChain.assemble(AwsCredentialsIdentity.class,
List.of(
provider("env",
StandardProvider.ENVIRONMENT,
diff --git a/aws/aws-credential-chain/src/test/java/software/amazon/smithy/java/aws/credentials/chain/AwsCredentialChainTest.java b/aws/aws-credential-chain/src/test/java/software/amazon/smithy/java/aws/credentials/chain/IdentityChainTest.java
similarity index 91%
rename from aws/aws-credential-chain/src/test/java/software/amazon/smithy/java/aws/credentials/chain/AwsCredentialChainTest.java
rename to aws/aws-credential-chain/src/test/java/software/amazon/smithy/java/aws/credentials/chain/IdentityChainTest.java
index d65cdaa9b9..6195e748dc 100644
--- a/aws/aws-credential-chain/src/test/java/software/amazon/smithy/java/aws/credentials/chain/AwsCredentialChainTest.java
+++ b/aws/aws-credential-chain/src/test/java/software/amazon/smithy/java/aws/credentials/chain/IdentityChainTest.java
@@ -19,10 +19,10 @@
import software.amazon.smithy.java.aws.auth.api.identity.AwsCredentialsIdentity;
import software.amazon.smithy.java.context.Context;
-class AwsCredentialChainTest {
+class IdentityChainTest {
@Test
void standardProvidersAreOrderedByEnumOrder() {
- var chain = CredentialChain.assemble(AwsCredentialsIdentity.class,
+ var chain = IdentityChain.assemble(AwsCredentialsIdentity.class,
List.of(
registration("imds",
new OrderingConstraint.Standard(StandardProvider.EC2_INSTANCE_METADATA),
@@ -40,7 +40,7 @@ void standardProvidersAreOrderedByEnumOrder() {
@Test
void firstSuccessfulProviderWins() {
- var chain = CredentialChain.assemble(AwsCredentialsIdentity.class,
+ var chain = IdentityChain.assemble(AwsCredentialsIdentity.class,
List.of(
registration("env",
new OrderingConstraint.Standard(StandardProvider.ENVIRONMENT),
@@ -57,7 +57,7 @@ void firstSuccessfulProviderWins() {
@Test
void allFailReturnsAggregatedError() {
- var chain = CredentialChain.assemble(AwsCredentialsIdentity.class,
+ var chain = IdentityChain.assemble(AwsCredentialsIdentity.class,
List.of(
registration("env",
new OrderingConstraint.Standard(StandardProvider.ENVIRONMENT),
@@ -76,7 +76,7 @@ void allFailReturnsAggregatedError() {
@Test
void duplicateSlotThrows() {
assertThrows(IllegalStateException.class,
- () -> CredentialChain.assemble(AwsCredentialsIdentity.class,
+ () -> IdentityChain.assemble(AwsCredentialsIdentity.class,
List.of(
registration("a",
new OrderingConstraint.Standard(StandardProvider.ENVIRONMENT),
@@ -89,7 +89,7 @@ void duplicateSlotThrows() {
@Test
void relativeAfterInsertsCorrectly() {
- var chain = CredentialChain.assemble(AwsCredentialsIdentity.class,
+ var chain = IdentityChain.assemble(AwsCredentialsIdentity.class,
List.of(
registration("env",
new OrderingConstraint.Standard(StandardProvider.ENVIRONMENT),
@@ -107,7 +107,7 @@ void relativeAfterInsertsCorrectly() {
@Test
void relativeBeforeInsertsCorrectly() {
- var chain = CredentialChain.assemble(AwsCredentialsIdentity.class,
+ var chain = IdentityChain.assemble(AwsCredentialsIdentity.class,
List.of(
registration("env",
new OrderingConstraint.Standard(StandardProvider.ENVIRONMENT),
@@ -125,7 +125,7 @@ void relativeBeforeInsertsCorrectly() {
@Test
void relativeToUnclaimedSlotAppendsAtEnd() {
- var chain = CredentialChain.assemble(AwsCredentialsIdentity.class,
+ var chain = IdentityChain.assemble(AwsCredentialsIdentity.class,
List.of(
registration("env",
new OrderingConstraint.Standard(StandardProvider.ENVIRONMENT),
@@ -141,7 +141,7 @@ void relativeToUnclaimedSlotAppendsAtEnd() {
@Test
void duplicateNameThrows() {
assertThrows(IllegalStateException.class,
- () -> CredentialChain.assemble(AwsCredentialsIdentity.class,
+ () -> IdentityChain.assemble(AwsCredentialsIdentity.class,
List.of(
registration("env",
new OrderingConstraint.Standard(StandardProvider.ENVIRONMENT),
@@ -154,7 +154,7 @@ void duplicateNameThrows() {
@Test
void emptyChainReturnsDescriptiveError() {
- var chain = CredentialChain.assemble(AwsCredentialsIdentity.class, List.of(), null);
+ var chain = IdentityChain.assemble(AwsCredentialsIdentity.class, List.of(), null);
IdentityResult result = chain.resolveIdentity(Context.empty());
assertNull(result.identity());
@@ -182,7 +182,7 @@ public void setup(Class extends Identity> identityType, ChainSetup setup) {
}
private static IdentityResolver errorResolver(String msg) {
- IdentityResult result = IdentityResult.ofError(AwsCredentialChainTest.class, msg);
+ IdentityResult result = IdentityResult.ofError(IdentityChainTest.class, msg);
return new IdentityResolver<>() {
public IdentityResult resolveIdentity(Context ctx) {
return result;
diff --git a/aws/aws-credentials-sts/src/main/java/software/amazon/smithy/java/aws/credentials/sts/StsAssumeRoleResolver.java b/aws/aws-credentials-sts/src/main/java/software/amazon/smithy/java/aws/credentials/sts/StsAssumeRoleResolver.java
index be851e50c7..99f2565161 100644
--- a/aws/aws-credentials-sts/src/main/java/software/amazon/smithy/java/aws/credentials/sts/StsAssumeRoleResolver.java
+++ b/aws/aws-credentials-sts/src/main/java/software/amazon/smithy/java/aws/credentials/sts/StsAssumeRoleResolver.java
@@ -48,6 +48,11 @@ public Class identityType() {
return AwsCredentialsIdentity.class;
}
+ // Visible for testing: the STS endpoint configuration this resolver was assembled with.
+ StsEndpointConfig endpoint() {
+ return endpoint;
+ }
+
@Override
public IdentityResult resolveIdentity(Context ctx) {
AwsCredentialsIdentity sourceCredentials = resolveSourceCredentials(source, new HashSet<>());
diff --git a/aws/aws-credentials-sts/src/main/java/software/amazon/smithy/java/aws/credentials/sts/StsEndpointConfig.java b/aws/aws-credentials-sts/src/main/java/software/amazon/smithy/java/aws/credentials/sts/StsEndpointConfig.java
index 2c4afaea67..41a4b8e443 100644
--- a/aws/aws-credentials-sts/src/main/java/software/amazon/smithy/java/aws/credentials/sts/StsEndpointConfig.java
+++ b/aws/aws-credentials-sts/src/main/java/software/amazon/smithy/java/aws/credentials/sts/StsEndpointConfig.java
@@ -15,6 +15,7 @@
*
* - The region attached to the credential source (profile-level
* {@code region} carried on {@code AssumeRole} / {@code WebIdentityToken}).
+ * - The client-specified region override ({@link ChainSetup#regionOverride()}).
* - {@code AWS_REGION} env var.
* - {@code AWS_DEFAULT_REGION} env var.
* - The active profile's {@code region} property.
@@ -37,6 +38,9 @@ record StsEndpointConfig(String region, boolean useGlobalEndpoint) {
static StsEndpointConfig resolve(String sourceRegion, ChainSetup setup) {
String region = sourceRegion;
+ if (region == null) {
+ region = setup.regionOverride();
+ }
if (region == null) {
region = setup.getenv("AWS_REGION");
}
diff --git a/aws/aws-credentials-sts/src/test/java/software/amazon/smithy/java/aws/credentials/sts/ProfileAssumeRoleProviderTest.java b/aws/aws-credentials-sts/src/test/java/software/amazon/smithy/java/aws/credentials/sts/ProfileAssumeRoleProviderTest.java
index 776c1a9d88..0c78507ac2 100644
--- a/aws/aws-credentials-sts/src/test/java/software/amazon/smithy/java/aws/credentials/sts/ProfileAssumeRoleProviderTest.java
+++ b/aws/aws-credentials-sts/src/test/java/software/amazon/smithy/java/aws/credentials/sts/ProfileAssumeRoleProviderTest.java
@@ -64,6 +64,34 @@ void registersWhenProfileHasRoleArn(@TempDir Path tmp) throws IOException {
assertEquals(1, setup.resolvers().size());
}
+ @Test
+ void regionOverrideFlowsIntoStsEndpoint(@TempDir Path tmp) throws IOException {
+ Path config = tmp.resolve("config");
+ Files.writeString(config, """
+ [default]
+ role_arn = arn:aws:iam::123456789:role/RoleA
+ source_profile = creds
+
+ [profile creds]
+ aws_access_key_id = AK
+ aws_secret_access_key = SK
+ """);
+
+ var profileFile = AwsProfileFile.builder().configFile(config).credentialsFile(null).build();
+ // No AWS_REGION/profile region; the client's region override should drive the STS endpoint.
+ var setup = ChainSetup.builder().env(k -> null).regionOverride("eu-central-1").build();
+ setup.setProfileFile(profileFile);
+ setup.setProfile(profileFile.activeProfile(k -> null));
+ var provider = new ProfileAssumeRoleProvider();
+ setup.setCurrentProvider(provider);
+
+ provider.setup(AwsCredentialsIdentity.class, setup);
+
+ assertEquals(1, setup.resolvers().size());
+ var resolver = (StsAssumeRoleResolver) setup.resolvers().get(0).resolver();
+ assertEquals("eu-central-1", resolver.endpoint().region());
+ }
+
@Test
void skipsWhenNoRoleArn(@TempDir Path tmp) throws IOException {
Path config = tmp.resolve("config");
diff --git a/aws/aws-credentials-sts/src/test/java/software/amazon/smithy/java/aws/credentials/sts/StsEndpointConfigTest.java b/aws/aws-credentials-sts/src/test/java/software/amazon/smithy/java/aws/credentials/sts/StsEndpointConfigTest.java
index 7e6423a9cd..278f16a1da 100644
--- a/aws/aws-credentials-sts/src/test/java/software/amazon/smithy/java/aws/credentials/sts/StsEndpointConfigTest.java
+++ b/aws/aws-credentials-sts/src/test/java/software/amazon/smithy/java/aws/credentials/sts/StsEndpointConfigTest.java
@@ -29,6 +29,23 @@ void sourceRegionWinsOverEverything(@TempDir Path tmp) throws IOException {
assertFalse(cfg.useGlobalEndpoint());
}
+ @Test
+ void regionOverrideWinsOverEnvAndProfile(@TempDir Path tmp) throws IOException {
+ var setup = setupWith(
+ Map.of("AWS_REGION", "us-west-2"),
+ profileWith(tmp, "region = eu-west-1"),
+ "ap-southeast-2");
+ var cfg = StsEndpointConfig.resolve(null, setup);
+ assertEquals("ap-southeast-2", cfg.region());
+ }
+
+ @Test
+ void sourceRegionWinsOverRegionOverride(@TempDir Path tmp) throws IOException {
+ var setup = setupWith(Map.of(), null, "ap-southeast-2");
+ var cfg = StsEndpointConfig.resolve("ap-south-1", setup);
+ assertEquals("ap-south-1", cfg.region());
+ }
+
@Test
void awsRegionEnvWinsOverDefaultRegionAndProfile(@TempDir Path tmp) throws IOException {
var setup = setupWith(
@@ -98,8 +115,12 @@ void envOverridesProfileStsRegionalEndpoints(@TempDir Path tmp) throws IOExcepti
}
private static ChainSetup setupWith(Map env, AwsProfileFile profileFile) {
+ return setupWith(env, profileFile, null);
+ }
+
+ private static ChainSetup setupWith(Map env, AwsProfileFile profileFile, String regionOverride) {
Map envCopy = new HashMap<>(env);
- var setup = ChainSetup.builder().env(envCopy::get).build();
+ var setup = ChainSetup.builder().env(envCopy::get).regionOverride(regionOverride).build();
if (profileFile != null) {
setup.setProfileFile(profileFile);
setup.setProfile(profileFile.activeProfile(k -> null));
diff --git a/aws/client/aws-client-core/src/main/java/software/amazon/smithy/java/aws/client/core/AwsCredentialChainPlugin.java b/aws/client/aws-client-core/src/main/java/software/amazon/smithy/java/aws/client/core/AwsCredentialChainPlugin.java
index aec878eddc..a836c416ba 100644
--- a/aws/client/aws-client-core/src/main/java/software/amazon/smithy/java/aws/client/core/AwsCredentialChainPlugin.java
+++ b/aws/client/aws-client-core/src/main/java/software/amazon/smithy/java/aws/client/core/AwsCredentialChainPlugin.java
@@ -7,7 +7,8 @@
import software.amazon.smithy.java.auth.api.identity.IdentityResolver;
import software.amazon.smithy.java.aws.auth.api.identity.AwsCredentialsIdentity;
-import software.amazon.smithy.java.aws.credentials.chain.CredentialChain;
+import software.amazon.smithy.java.aws.client.core.settings.RegionSetting;
+import software.amazon.smithy.java.aws.credentials.chain.IdentityChain;
import software.amazon.smithy.java.client.core.ClientConfig;
import software.amazon.smithy.java.client.core.ClientPlugin;
import software.amazon.smithy.java.client.core.auth.scheme.AuthScheme;
@@ -32,13 +33,19 @@
public final class AwsCredentialChainPlugin implements ClientPlugin {
@Override
public Phase getPluginPhase() {
- return Phase.DEFAULTS;
+ // Run after DEFAULTS so the client's region (and any other defaults) are populated on the config before we
+ // read them to assemble the chain.
+ return Phase.AFTER_DEFAULTS;
}
@Override
public void configureClient(ClientConfig.Builder config) {
if (needsAwsCredentials(config) && !hasAwsCredentialsResolver(config)) {
- var chain = CredentialChain.create(AwsCredentialsIdentity.class);
+ // Pass the client's configured region so credential providers that make a service call (STS, SSO) use
+ // the same region as the client. Null when no region is configured, in which case the providers fall
+ // back to the environment and profile.
+ String region = config.context().get(RegionSetting.REGION);
+ var chain = IdentityChain.create(AwsCredentialsIdentity.class, null, region);
config.addIdentityResolver(chain);
config.addInterceptor(new InvalidateOnAuthFailureInterceptor(chain));
}
diff --git a/aws/client/aws-client-core/src/test/java/software/amazon/smithy/java/aws/client/core/identity/CredentialChainFallthroughTest.java b/aws/client/aws-client-core/src/test/java/software/amazon/smithy/java/aws/client/core/identity/IdentityChainFallthroughTest.java
similarity index 90%
rename from aws/client/aws-client-core/src/test/java/software/amazon/smithy/java/aws/client/core/identity/CredentialChainFallthroughTest.java
rename to aws/client/aws-client-core/src/test/java/software/amazon/smithy/java/aws/client/core/identity/IdentityChainFallthroughTest.java
index 376c4455fe..36deeb6eb9 100644
--- a/aws/client/aws-client-core/src/test/java/software/amazon/smithy/java/aws/client/core/identity/CredentialChainFallthroughTest.java
+++ b/aws/client/aws-client-core/src/test/java/software/amazon/smithy/java/aws/client/core/identity/IdentityChainFallthroughTest.java
@@ -10,10 +10,10 @@
import org.junit.jupiter.api.Test;
import software.amazon.smithy.java.aws.auth.api.identity.AwsCredentialsIdentity;
import software.amazon.smithy.java.aws.credentials.chain.ChainSetup;
-import software.amazon.smithy.java.aws.credentials.chain.CredentialChain;
+import software.amazon.smithy.java.aws.credentials.chain.IdentityChain;
/**
- * Demonstrates that the assembled credential chain fails to fall through past the
+ * Demonstrates that the assembled {@link IdentityChain} fails to fall through past the
* {@code JAVA_SYSTEM_PROPERTIES} slot.
*
* {@link SystemPropertiesCredentialProvider} registers its resolver via
@@ -23,7 +23,7 @@
*
*
This test encodes the correct behavior and therefore fails against the current implementation.
*/
-public class CredentialChainFallthroughTest {
+public class IdentityChainFallthroughTest {
/**
* When system properties are absent, the assembled chain should contain the {@code Environment} provider so
@@ -37,7 +37,7 @@ void assembledChainShouldFallThroughToEnvironmentProvider() {
// no aws.accessKeyId/aws.secretAccessKey system properties, then restore.
String savedAccessKey = System.clearProperty("aws.accessKeyId");
String savedSecretKey = System.clearProperty("aws.secretAccessKey");
- try (var chain = CredentialChain.create(AwsCredentialsIdentity.class)) {
+ try (var chain = IdentityChain.create(AwsCredentialsIdentity.class)) {
var providers = chain.providerNames();
assertTrue(providers.contains("Environment"),
"Environment provider was dropped from the chain. SystemPropertiesCredentialProvider "
diff --git a/aws/client/aws-client-core/src/test/java/software/amazon/smithy/java/aws/credentials/chain/CredentialChainEndToEndTest.java b/aws/client/aws-client-core/src/test/java/software/amazon/smithy/java/aws/credentials/chain/IdentityChainEndToEndTest.java
similarity index 93%
rename from aws/client/aws-client-core/src/test/java/software/amazon/smithy/java/aws/credentials/chain/CredentialChainEndToEndTest.java
rename to aws/client/aws-client-core/src/test/java/software/amazon/smithy/java/aws/credentials/chain/IdentityChainEndToEndTest.java
index 2f1ab8d1db..961e27a0e2 100644
--- a/aws/client/aws-client-core/src/test/java/software/amazon/smithy/java/aws/credentials/chain/CredentialChainEndToEndTest.java
+++ b/aws/client/aws-client-core/src/test/java/software/amazon/smithy/java/aws/credentials/chain/IdentityChainEndToEndTest.java
@@ -28,19 +28,19 @@
/**
* End-to-end assembly + resolution test that wires the real credential providers together through the
- * real {@link CredentialChain#assemble} and exercises fall-through across the sources.
+ * real {@link IdentityChain#assemble} and exercises fall-through across the sources.
*
*
Scope: this module ({@code aws-client-core}) sees the system-property, environment, and profile
* (static/session keys) providers. It does not depend on {@code aws-credentials-sts} or
* {@code aws-credentials-imds}, so the STS and IMDS slots are not covered here — a fuller test spanning those
* would need a module that depends on all of them.
*
- *
Determinism comes from the package-private {@link CredentialChain#assemble} overload that accepts a
+ *
Determinism comes from the package-private {@link IdentityChain#assemble} overload that accepts a
* caller-built {@link ChainSetup}: the environment is injected and the profile is pre-set, so nothing reads the
* real process environment or {@code ~/.aws} files. The profile is set directly rather than via
* {@code SharedConfigProvider} (which loads real config files and cannot be pointed at a temp path).
*/
-class CredentialChainEndToEndTest {
+class IdentityChainEndToEndTest {
private static final List PROVIDERS = List.of(
new SystemPropertiesCredentialProvider(),
@@ -103,7 +103,7 @@ void returnsAggregatedErrorWhenNoSourceResolves() {
assertNull(result.identity());
}
- private static CredentialChain assembleWith(
+ private static IdentityChain assembleWith(
Function env,
AwsProfileFile profileFile
) {
@@ -112,7 +112,7 @@ private static CredentialChain assembleWith(
setup.setProfileFile(profileFile);
setup.setProfile(profileFile.profile("default"));
}
- return CredentialChain.assemble(AwsCredentialsIdentity.class, PROVIDERS, null, setup);
+ return IdentityChain.assemble(AwsCredentialsIdentity.class, PROVIDERS, null, setup);
}
private static AwsProfileFile writeConfig(Path tmp, String contents) throws IOException {