) credentialsProvider;
- return provider.invalidate(identity);
+ if (credentialsProvider != null) {
+ return credentialsProvider.invalidate(identity);
}
return CompletableFuture.completedFuture(null);
}
diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/CredentialsInvalidationUtils.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/CredentialsInvalidationUtils.java
index 0293d83e1b77..d76b8f45e459 100644
--- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/CredentialsInvalidationUtils.java
+++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/CredentialsInvalidationUtils.java
@@ -35,20 +35,22 @@ private CredentialsInvalidationUtils() {
}
/**
- * Invalidates the given cache if the rejected identity's access key ID matches the cached credential's access key ID.
+ * Invalidates the given cache if the rejected identity's access key ID matches the access key ID of the cached value.
*
* This method encapsulates the common pattern used by caching credential providers:
*
* - Extract the rejected access key ID from the identity
- * - Compare it against the currently cached credentials
+ * - Use the {@code accessKeyIdExtractor} to get the access key ID from the currently cached value
* - If they match, mark the cache for mandatory refresh
*
*
+ * For providers whose cache directly stores {@link AwsCredentialsIdentity} (or a subtype), pass
+ * {@code AwsCredentialsIdentity::accessKeyId} as the extractor. For providers that cache a wrapper type,
+ * provide an appropriate extraction function (e.g., {@code holder -> holder.sessionCredentials().accessKeyId()}).
+ *
* @param identity The identity that was rejected by the service.
* @param cache The cached supplier to invalidate.
- * @param credentialsExtractor A function that extracts the {@link AwsCredentialsIdentity} from the cached value type.
- * For providers that cache {@code AwsCredentials} directly, use {@code Function.identity()}.
- * For providers that cache a holder object, provide the appropriate extraction function.
+ * @param accessKeyIdExtractor A function that extracts the access key ID from the cached value.
* @param The type of value stored in the cache.
* @return A {@link CompletableFuture} that completes when the invalidation check is done, or completes exceptionally
* if an error occurs during the invalidation.
@@ -56,17 +58,11 @@ private CredentialsInvalidationUtils() {
public static CompletableFuture invalidateCredentialsCache(
AwsCredentialsIdentity identity,
CachedSupplier cache,
- Function credentialsExtractor) {
+ Function accessKeyIdExtractor) {
try {
String rejectedAccessKeyId = identity.accessKeyId();
- cache.invalidate(cachedValue -> {
- AwsCredentialsIdentity cachedIdentity = credentialsExtractor.apply(cachedValue);
- if (cachedIdentity == null) {
- return false;
- }
- return rejectedAccessKeyId.equals(cachedIdentity.accessKeyId());
- });
+ cache.invalidate(cachedValue -> rejectedAccessKeyId.equals(accessKeyIdExtractor.apply(cachedValue)));
return CompletableFuture.completedFuture(null);
} catch (Exception e) {
return CompletableFutureUtils.failedFuture(e);
diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/IdentityProviderInvalidateTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/IdentityProviderInvalidateTest.java
index c2927d50d600..7b9ee9c63d1e 100644
--- a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/IdentityProviderInvalidateTest.java
+++ b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/IdentityProviderInvalidateTest.java
@@ -66,13 +66,12 @@ class IdentityProviderInvalidateTest {
@BeforeEach
void setup() {
environmentVariableHelper.reset();
- System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property(),
- "http://localhost:" + wireMockServer.getPort());
+ environmentVariableHelper.set(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT,
+ "http://localhost:" + wireMockServer.getPort());
}
@AfterAll
static void teardown() {
- System.clearProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property());
environmentVariableHelper.reset();
}
diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/exception/AwsServiceException.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/exception/AwsServiceException.java
index 238666a555ce..26a884d0a103 100644
--- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/exception/AwsServiceException.java
+++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/exception/AwsServiceException.java
@@ -134,6 +134,19 @@ public boolean isThrottlingException() {
.orElse(false);
}
+ /**
+ * Checks if the exception indicates an authentication error where the credentials were rejected,
+ * based on the AWS error code (e.g., {@code ExpiredToken}, {@code InvalidToken}, {@code AuthFailure}).
+ *
+ * @return true if the AWS error code indicates an authentication error, otherwise false.
+ */
+ @Override
+ public boolean isAuthenticationError() {
+ return Optional.ofNullable(awsErrorDetails)
+ .map(a -> AwsErrorCode.isAuthenticationErrorCode(a.errorCode()))
+ .orElse(false);
+ }
+
/**
* @return {@link Builder} instance to construct a new {@link AwsServiceException}.
*/
diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsErrorCode.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsErrorCode.java
index acdac5954713..4a31dbd985d3 100644
--- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsErrorCode.java
+++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsErrorCode.java
@@ -31,6 +31,7 @@ public final class AwsErrorCode {
public static final Set THROTTLING_ERROR_CODES;
public static final Set DEFINITE_CLOCK_SKEW_ERROR_CODES;
public static final Set POSSIBLE_CLOCK_SKEW_ERROR_CODES;
+ public static final Set AUTH_ERROR_CODES;
static {
Set throttlingErrorCodes = new HashSet<>(9);
@@ -66,6 +67,12 @@ public final class AwsErrorCode {
retryableErrorCodes.add("RequestTimeoutException");
retryableErrorCodes.add("InternalError");
RETRYABLE_ERROR_CODES = unmodifiableSet(retryableErrorCodes);
+
+ Set authErrorCodes = new HashSet<>(3);
+ authErrorCodes.add("ExpiredToken");
+ authErrorCodes.add("InvalidToken");
+ authErrorCodes.add("AuthFailure");
+ AUTH_ERROR_CODES = unmodifiableSet(authErrorCodes);
}
private AwsErrorCode() {
@@ -86,4 +93,8 @@ public static boolean isPossibleClockSkewErrorCode(String errorCode) {
public static boolean isRetryableErrorCode(String errorCode) {
return RETRYABLE_ERROR_CODES.contains(errorCode);
}
+
+ public static boolean isAuthenticationErrorCode(String errorCode) {
+ return AUTH_ERROR_CODES.contains(errorCode);
+ }
}
diff --git a/core/identity-spi/src/main/java/software/amazon/awssdk/identity/spi/IdentityProvider.java b/core/identity-spi/src/main/java/software/amazon/awssdk/identity/spi/IdentityProvider.java
index 94582659213f..dcdc14fadc7c 100644
--- a/core/identity-spi/src/main/java/software/amazon/awssdk/identity/spi/IdentityProvider.java
+++ b/core/identity-spi/src/main/java/software/amazon/awssdk/identity/spi/IdentityProvider.java
@@ -166,8 +166,7 @@ default CompletableFuture extends IdentityT> resolveIdentity() {
* The next call to {@link #resolveIdentity} will attempt to fetch fresh credentials.
*
* Implementations MUST only invalidate if the currently-cached identity matches
- * the rejected identity (e.g., same access key ID). Implementations MUST NOT discard
- * cached credentials entirely, and MUST NOT clear or bypass any refresh backoff.
+ * the rejected identity (e.g., same access key ID).
*
* The default implementation is a no-op, suitable for providers that do not cache.
*
diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/exception/SdkServiceException.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/exception/SdkServiceException.java
index 6403c4e74fdc..2f5cc422a346 100644
--- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/exception/SdkServiceException.java
+++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/exception/SdkServiceException.java
@@ -102,6 +102,20 @@ public boolean isRetryableException() {
return false;
}
+ /**
+ * Specifies whether an exception indicates an authentication or authorization error where the credentials
+ * used for the request were rejected by the service. Subclasses can override this to check service-specific
+ * error codes (e.g., {@code ExpiredToken}, {@code InvalidToken}, {@code AuthFailure}).
+ *
+ * When this returns {@code true}, the SDK may invalidate cached credentials so that the next attempt
+ * resolves fresh ones.
+ *
+ * @return true if the exception is classified as an authentication error, otherwise false.
+ */
+ public boolean isAuthenticationError() {
+ return false;
+ }
+
/**
* @return {@link Builder} instance to construct a new {@link SdkServiceException}.
*/
diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/AuthErrorInvalidationHelper.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/AuthErrorInvalidationHelper.java
index 143d882cd8b8..2894323dcf92 100644
--- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/AuthErrorInvalidationHelper.java
+++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/AuthErrorInvalidationHelper.java
@@ -15,9 +15,6 @@
package software.amazon.awssdk.core.internal.http.pipeline.stages.utils;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.core.SelectedAuthScheme;
import software.amazon.awssdk.core.exception.SdkServiceException;
@@ -32,8 +29,9 @@
* Utility that detects authentication error responses and triggers credential invalidation
* on the identity provider that produced the rejected credentials.
*
- * When a service returns {@code ExpiredToken}, {@code InvalidToken}, or {@code AuthFailure},
- * this helper retrieves the {@link SelectedAuthScheme} from the execution context and calls
+ *
When a service returns an authentication error (as determined by
+ * {@link SdkServiceException#isAuthenticationError()}), this helper retrieves the
+ * {@link SelectedAuthScheme} from the execution context and calls
* {@link IdentityProvider#invalidate} so the next retry attempt resolves fresh credentials.
*
* All exceptions from the invalidation path are caught and logged at debug level.
@@ -44,12 +42,6 @@ public final class AuthErrorInvalidationHelper {
private static final Logger LOG = Logger.loggerFor(AuthErrorInvalidationHelper.class);
- private static final Set INVALIDATION_ERROR_CODES = new HashSet<>(Arrays.asList(
- "ExpiredToken",
- "InvalidToken",
- "AuthFailure"
- ));
-
private AuthErrorInvalidationHelper() {
}
@@ -67,9 +59,7 @@ public static void invalidateIfAuthError(Throwable exception, RequestExecutionCo
}
SdkServiceException serviceException = (SdkServiceException) exception;
- String errorCode = extractErrorCode(serviceException);
-
- if (errorCode == null || !INVALIDATION_ERROR_CODES.contains(errorCode)) {
+ if (!serviceException.isAuthenticationError()) {
return;
}
@@ -87,31 +77,6 @@ public static void invalidateIfAuthError(Throwable exception, RequestExecutionCo
}
}
- /**
- * Extracts the error code from an SdkServiceException. Since sdk-core does not depend on
- * aws-core, this uses reflection to access awsErrorDetails().errorCode() which is defined
- * on AwsServiceException.
- */
- private static String extractErrorCode(SdkServiceException serviceException) {
- try {
- java.lang.reflect.Method awsErrorDetailsMethod =
- serviceException.getClass().getMethod("awsErrorDetails");
- Object errorDetails = awsErrorDetailsMethod.invoke(serviceException);
- if (errorDetails == null) {
- return null;
- }
- java.lang.reflect.Method errorCodeMethod = errorDetails.getClass().getMethod("errorCode");
- Object errorCode = errorCodeMethod.invoke(errorDetails);
- return errorCode instanceof String ? (String) errorCode : null;
- } catch (NoSuchMethodException e) {
- // Exception type does not have awsErrorDetails() — not an AWS service exception
- return null;
- } catch (Exception e) {
- LOG.debug(() -> "Failed to extract error code from exception: " + e.getMessage(), e);
- return null;
- }
- }
-
@SuppressWarnings("unchecked")
private static void doInvalidate(SelectedAuthScheme selectedAuthScheme) {
T resolvedIdentity = CompletableFutureUtils.joinLikeSync(selectedAuthScheme.identity());
diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/AuthErrorInvalidationHelperTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/AuthErrorInvalidationHelperTest.java
index 058cc6d9c887..8c913effbf2b 100644
--- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/AuthErrorInvalidationHelperTest.java
+++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/AuthErrorInvalidationHelperTest.java
@@ -20,6 +20,9 @@
import static org.mockito.Mockito.mock;
import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@@ -211,9 +214,8 @@ private RequestExecutionContext contextWithNoAuthScheme() {
}
/**
- * Creates an SdkServiceException subclass with an {@code awsErrorDetails()} method that returns
- * an object with an {@code errorCode()} method. This works with the reflection-based approach
- * in AuthErrorInvalidationHelper.
+ * Creates an SdkServiceException subclass that overrides {@code isAuthenticationError()} to
+ * return true for the known auth error codes.
*/
private Throwable serviceExceptionWithErrorCode(String errorCode) {
return new TestAwsServiceException(errorCode);
@@ -286,36 +288,24 @@ public CompletableFuture invalidate(TestIdentity identity) {
}
/**
- * Simulates an AwsServiceException accessible via reflection. The AuthErrorInvalidationHelper
- * uses reflection to call awsErrorDetails().errorCode() since sdk-core doesn't depend on aws-core.
- * Any class with these methods will work via reflection.
+ * Simulates an AwsServiceException that reports authentication errors via the
+ * {@link SdkServiceException#isAuthenticationError()} virtual method.
*/
private static class TestAwsServiceException extends SdkServiceException {
- private final TestAwsErrorDetails awsErrorDetails;
+ private static final Set AUTH_ERROR_CODES = new HashSet<>(Arrays.asList(
+ "ExpiredToken", "InvalidToken", "AuthFailure"
+ ));
- TestAwsServiceException(String errorCode) {
- super(SdkServiceException.builder().message("test exception").statusCode(401));
- this.awsErrorDetails = new TestAwsErrorDetails(errorCode);
- }
-
- public TestAwsErrorDetails awsErrorDetails() {
- return awsErrorDetails;
- }
- }
-
- /**
- * Simulates AwsErrorDetails. The reflection in AuthErrorInvalidationHelper calls
- * awsErrorDetails().errorCode(), so this class just needs an errorCode() method.
- */
- private static class TestAwsErrorDetails {
private final String errorCode;
- TestAwsErrorDetails(String errorCode) {
+ TestAwsServiceException(String errorCode) {
+ super(SdkServiceException.builder().message("test exception").statusCode(401));
this.errorCode = errorCode;
}
- public String errorCode() {
- return errorCode;
+ @Override
+ public boolean isAuthenticationError() {
+ return AUTH_ERROR_CODES.contains(errorCode);
}
}
}
diff --git a/services/sso/src/main/java/software/amazon/awssdk/services/sso/auth/SsoCredentialsProvider.java b/services/sso/src/main/java/software/amazon/awssdk/services/sso/auth/SsoCredentialsProvider.java
index 7a7ddcda6f09..f133a0531652 100644
--- a/services/sso/src/main/java/software/amazon/awssdk/services/sso/auth/SsoCredentialsProvider.java
+++ b/services/sso/src/main/java/software/amazon/awssdk/services/sso/auth/SsoCredentialsProvider.java
@@ -174,7 +174,7 @@ public AwsCredentials resolveCredentials() {
@Override
public CompletableFuture invalidate(AwsCredentialsIdentity identity) {
return CredentialsInvalidationUtils.invalidateCredentialsCache(
- identity, credentialCache, holder -> holder.sessionCredentials());
+ identity, credentialCache, holder -> holder.sessionCredentials().accessKeyId());
}
@Override
diff --git a/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsCredentialsProvider.java b/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsCredentialsProvider.java
index 2bccf484a768..8fd1acda683d 100644
--- a/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsCredentialsProvider.java
+++ b/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsCredentialsProvider.java
@@ -129,7 +129,7 @@ public void close() {
@Override
public CompletableFuture invalidate(AwsCredentialsIdentity identity) {
return CredentialsInvalidationUtils.invalidateCredentialsCache(
- identity, sessionCache, cachedCreds -> (AwsCredentialsIdentity) cachedCreds);
+ identity, sessionCache, AwsCredentialsIdentity::accessKeyId);
}
/**
From 192c33f27db1cbbe47fc5e90393bbb879b29dc12 Mon Sep 17 00:00:00 2001
From: Alex Woods
Date: Thu, 9 Jul 2026 09:27:58 -0700
Subject: [PATCH 4/9] Cleanups and fixes
---
.../AwsCredentialsProviderChain.java | 15 +-
.../ContainerCredentialsProvider.java | 7 +-
.../InstanceProfileCredentialsProvider.java | 7 +-
.../ProcessCredentialsProvider.java | 7 +-
.../CredentialsInvalidationUtils.java | 71 ---
.../AwsCredentialsProviderChainTest.java | 65 ++
.../IdentityProviderInvalidateTest.java | 300 ----------
...nstanceProfileCredentialsProviderTest.java | 55 ++
.../LazyAwsCredentialsProviderTest.java | 25 +
.../awssdk/awscore/internal/AwsErrorCode.java | 1 -
.../core/exception/SdkServiceException.java | 6 +-
.../utils/AuthErrorInvalidationHelper.java | 6 +-
.../AuthErrorInvalidationHelperTest.java | 24 +-
.../signin/auth/LoginCredentialsProvider.java | 14 +-
.../sso/auth/SsoCredentialsProvider.java | 7 +-
.../sso/auth/SsoCredentialsProviderTest.java | 69 +++
.../sts/auth/StsCredentialsProvider.java | 7 +-
.../auth/StsCredentialsProviderTestBase.java | 51 ++
.../AuthErrorInvalidationIntegrationTest.java | 131 +---
.../awssdk/utils/cache/CachedSupplier.java | 15 +-
.../cache/CachedSupplierInvalidateTest.java | 559 ------------------
.../utils/cache/CachedSupplierTest.java | 145 +++++
22 files changed, 492 insertions(+), 1095 deletions(-)
delete mode 100644 core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/CredentialsInvalidationUtils.java
delete mode 100644 core/auth/src/test/java/software/amazon/awssdk/auth/credentials/IdentityProviderInvalidateTest.java
delete mode 100644 utils/src/test/java/software/amazon/awssdk/utils/cache/CachedSupplierInvalidateTest.java
diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/AwsCredentialsProviderChain.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/AwsCredentialsProviderChain.java
index 468fdbbfb3f4..5d4e46bd2395 100644
--- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/AwsCredentialsProviderChain.java
+++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/AwsCredentialsProviderChain.java
@@ -132,13 +132,11 @@ public AwsCredentials resolveCredentials() {
}
@Override
- @SuppressWarnings("unchecked")
public CompletableFuture invalidate(AwsCredentialsIdentity identity) {
CompletableFuture>[] futures = credentialsProviders.stream()
.map(provider -> {
try {
- return ((IdentityProvider) provider)
- .invalidate(identity)
+ return invalidateProvider(provider, identity)
.exceptionally(e -> {
log.debug(() -> "Failed to invalidate provider " + provider + ": " + e.getMessage(), e);
return null;
@@ -152,6 +150,17 @@ public CompletableFuture invalidate(AwsCredentialsIdentity identity) {
return CompletableFuture.allOf(futures);
}
+ /**
+ * Helper to call invalidate with proper type capture, avoiding unchecked casts.
+ * The identity is always an {@code AwsCredentialsIdentity}, and all providers in this chain
+ * are {@code IdentityProvider extends AwsCredentialsIdentity>}, so the cast is safe.
+ */
+ @SuppressWarnings("unchecked")
+ private static CompletableFuture invalidateProvider(
+ IdentityProvider provider, AwsCredentialsIdentity identity) {
+ return provider.invalidate((T) identity);
+ }
+
@Override
public void close() {
credentialsProviders.forEach(c -> IoUtils.closeIfCloseable(c, null));
diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ContainerCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ContainerCredentialsProvider.java
index c69193f41d38..9a95620ecb60 100644
--- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ContainerCredentialsProvider.java
+++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ContainerCredentialsProvider.java
@@ -38,7 +38,6 @@
import java.util.function.Predicate;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.auth.credentials.internal.ContainerCredentialsRetryPolicy;
-import software.amazon.awssdk.auth.credentials.internal.CredentialsInvalidationUtils;
import software.amazon.awssdk.auth.credentials.internal.HttpCredentialsLoader;
import software.amazon.awssdk.auth.credentials.internal.HttpCredentialsLoader.LoadedCredentials;
import software.amazon.awssdk.core.SdkSystemSetting;
@@ -191,8 +190,10 @@ public AwsCredentials resolveCredentials() {
@Override
public CompletableFuture invalidate(AwsCredentialsIdentity identity) {
- return CredentialsInvalidationUtils.invalidateCredentialsCache(
- identity, credentialsCache, AwsCredentialsIdentity::accessKeyId);
+ String rejectedAccessKeyId = identity.accessKeyId();
+ return CompletableFuture.runAsync(() ->
+ credentialsCache.invalidate(cachedCreds -> rejectedAccessKeyId.equals(cachedCreds.accessKeyId()))
+ );
}
@Override
diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java
index 977660058622..19baaff757fe 100644
--- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java
+++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java
@@ -30,7 +30,6 @@
import java.util.function.Supplier;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.annotations.SdkTestInternalApi;
-import software.amazon.awssdk.auth.credentials.internal.CredentialsInvalidationUtils;
import software.amazon.awssdk.auth.credentials.internal.Ec2MetadataConfigProvider;
import software.amazon.awssdk.auth.credentials.internal.HttpCredentialsLoader;
import software.amazon.awssdk.auth.credentials.internal.HttpCredentialsLoader.LoadedCredentials;
@@ -172,8 +171,10 @@ public AwsCredentials resolveCredentials() {
@Override
public CompletableFuture invalidate(AwsCredentialsIdentity identity) {
- return CredentialsInvalidationUtils.invalidateCredentialsCache(
- identity, credentialsCache, AwsCredentialsIdentity::accessKeyId);
+ String rejectedAccessKeyId = identity.accessKeyId();
+ return CompletableFuture.runAsync(() ->
+ credentialsCache.invalidate(cachedCreds -> rejectedAccessKeyId.equals(cachedCreds.accessKeyId()))
+ );
}
private RefreshResult refreshCredentials() {
diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProcessCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProcessCredentialsProvider.java
index de1a4350e771..e32a319a7e4c 100644
--- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProcessCredentialsProvider.java
+++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProcessCredentialsProvider.java
@@ -27,7 +27,6 @@
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import software.amazon.awssdk.annotations.SdkPublicApi;
-import software.amazon.awssdk.auth.credentials.internal.CredentialsInvalidationUtils;
import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId;
import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity;
import software.amazon.awssdk.protocols.jsoncore.JsonNode;
@@ -171,8 +170,10 @@ public AwsCredentials resolveCredentials() {
@Override
public CompletableFuture invalidate(AwsCredentialsIdentity identity) {
- return CredentialsInvalidationUtils.invalidateCredentialsCache(
- identity, processCredentialCache, AwsCredentialsIdentity::accessKeyId);
+ String rejectedAccessKeyId = identity.accessKeyId();
+ return CompletableFuture.runAsync(() ->
+ processCredentialCache.invalidate(cachedCreds -> rejectedAccessKeyId.equals(cachedCreds.accessKeyId()))
+ );
}
private RefreshResult refreshCredentials() {
diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/CredentialsInvalidationUtils.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/CredentialsInvalidationUtils.java
deleted file mode 100644
index d76b8f45e459..000000000000
--- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/CredentialsInvalidationUtils.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed
- * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-package software.amazon.awssdk.auth.credentials.internal;
-
-import java.util.concurrent.CompletableFuture;
-import java.util.function.Function;
-import software.amazon.awssdk.annotations.SdkProtectedApi;
-import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity;
-import software.amazon.awssdk.utils.CompletableFutureUtils;
-import software.amazon.awssdk.utils.cache.CachedSupplier;
-
-/**
- * Utility methods for credential provider invalidation logic.
- *
- * This class provides shared implementations for the common pattern of comparing a rejected
- * identity's access key ID against a cached credential and conditionally invalidating the cache.
- */
-@SdkProtectedApi
-public final class CredentialsInvalidationUtils {
-
- private CredentialsInvalidationUtils() {
- }
-
- /**
- * Invalidates the given cache if the rejected identity's access key ID matches the access key ID of the cached value.
- *
- * This method encapsulates the common pattern used by caching credential providers:
- *
- * - Extract the rejected access key ID from the identity
- * - Use the {@code accessKeyIdExtractor} to get the access key ID from the currently cached value
- * - If they match, mark the cache for mandatory refresh
- *
- *
- * For providers whose cache directly stores {@link AwsCredentialsIdentity} (or a subtype), pass
- * {@code AwsCredentialsIdentity::accessKeyId} as the extractor. For providers that cache a wrapper type,
- * provide an appropriate extraction function (e.g., {@code holder -> holder.sessionCredentials().accessKeyId()}).
- *
- * @param identity The identity that was rejected by the service.
- * @param cache The cached supplier to invalidate.
- * @param accessKeyIdExtractor A function that extracts the access key ID from the cached value.
- * @param The type of value stored in the cache.
- * @return A {@link CompletableFuture} that completes when the invalidation check is done, or completes exceptionally
- * if an error occurs during the invalidation.
- */
- public static CompletableFuture invalidateCredentialsCache(
- AwsCredentialsIdentity identity,
- CachedSupplier cache,
- Function accessKeyIdExtractor) {
-
- try {
- String rejectedAccessKeyId = identity.accessKeyId();
- cache.invalidate(cachedValue -> rejectedAccessKeyId.equals(accessKeyIdExtractor.apply(cachedValue)));
- return CompletableFuture.completedFuture(null);
- } catch (Exception e) {
- return CompletableFutureUtils.failedFuture(e);
- }
- }
-}
diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/AwsCredentialsProviderChainTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/AwsCredentialsProviderChainTest.java
index 8a59ad26c15c..a8e19f5ddb87 100644
--- a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/AwsCredentialsProviderChainTest.java
+++ b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/AwsCredentialsProviderChainTest.java
@@ -18,8 +18,14 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import java.util.Arrays;
+import java.util.concurrent.CompletableFuture;
import org.junit.Test;
import org.junit.jupiter.api.function.Executable;
import software.amazon.awssdk.core.exception.SdkClientException;
@@ -172,6 +178,65 @@ private static void assertChainResolvesCorrectly(AwsCredentialsProviderChain cha
assertThat(credentials.secretAccessKey()).isEqualTo("secretKey");
}
+ @Test
+ public void invalidate_propagatesToAllChildren() {
+ TrackingCredentialsProvider provider1 = new TrackingCredentialsProvider("key1", "secret1");
+ TrackingCredentialsProvider provider2 = new TrackingCredentialsProvider("key2", "secret2");
+ TrackingCredentialsProvider provider3 = new TrackingCredentialsProvider("key3", "secret3");
+
+ AwsCredentialsProviderChain chain = AwsCredentialsProviderChain.builder()
+ .credentialsProviders(provider1, provider2, provider3)
+ .build();
+
+ AwsCredentialsIdentity identity = AwsBasicCredentials.create("key1", "secret1");
+ chain.invalidate(identity).join();
+
+ assertThat(provider1.invalidateCallCount).isEqualTo(1);
+ assertThat(provider2.invalidateCallCount).isEqualTo(1);
+ assertThat(provider3.invalidateCallCount).isEqualTo(1);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void invalidate_doesNotShortCircuit_whenChildThrows() {
+ AwsCredentialsProvider mockProvider1 = mock(AwsCredentialsProvider.class);
+ AwsCredentialsProvider mockProvider2 = mock(AwsCredentialsProvider.class);
+ AwsCredentialsProvider mockProvider3 = mock(AwsCredentialsProvider.class);
+
+ doThrow(new RuntimeException("Provider 1 failed")).when(mockProvider1).invalidate(any());
+
+ AwsCredentialsProviderChain chain = AwsCredentialsProviderChain.builder()
+ .credentialsProviders(mockProvider1, mockProvider2, mockProvider3)
+ .build();
+
+ AwsCredentialsIdentity identity = AwsBasicCredentials.create("key1", "secret1");
+ chain.invalidate(identity).join();
+
+ verify(mockProvider1, times(1)).invalidate(identity);
+ verify(mockProvider2, times(1)).invalidate(identity);
+ verify(mockProvider3, times(1)).invalidate(identity);
+ }
+
+ private static final class TrackingCredentialsProvider implements AwsCredentialsProvider {
+ private final AwsBasicCredentials credentials;
+ int invalidateCallCount = 0;
+
+ TrackingCredentialsProvider(String accessKeyId, String secretAccessKey) {
+ this.credentials = AwsBasicCredentials.create(accessKeyId, secretAccessKey);
+ }
+
+ @Override
+ public AwsCredentials resolveCredentials() {
+ return credentials;
+ }
+
+ @Override
+ public CompletableFuture invalidate(AwsCredentialsIdentity identity) {
+ invalidateCallCount++;
+ return CompletableFuture.completedFuture(null);
+ }
+ }
+
private static final class MockCredentialsProvider implements AwsCredentialsProvider {
private final StaticCredentialsProvider staticCredentialsProvider;
private final String exceptionMessage;
diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/IdentityProviderInvalidateTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/IdentityProviderInvalidateTest.java
deleted file mode 100644
index 7b9ee9c63d1e..000000000000
--- a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/IdentityProviderInvalidateTest.java
+++ /dev/null
@@ -1,300 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed
- * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-package software.amazon.awssdk.auth.credentials;
-
-import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
-import static com.github.tomakehurst.wiremock.client.WireMock.get;
-import static com.github.tomakehurst.wiremock.client.WireMock.put;
-import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
-import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
-import com.github.tomakehurst.wiremock.junit5.WireMockTest;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CompletableFuture;
-import org.junit.jupiter.api.AfterAll;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
-import software.amazon.awssdk.auth.credentials.internal.LazyAwsCredentialsProvider;
-import software.amazon.awssdk.core.SdkSystemSetting;
-import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity;
-import software.amazon.awssdk.utils.DateUtils;
-import software.amazon.awssdk.testutils.EnvironmentVariableHelper;
-
-/**
- * Unit tests for identity provider invalidation: chain propagation, LazyAwsCredentialsProvider delegation,
- * and individual provider accessKeyId matching.
- */
-@WireMockTest
-class IdentityProviderInvalidateTest {
-
- private static final String TOKEN_RESOURCE_PATH = "/latest/api/token";
- private static final String CREDENTIALS_RESOURCE_PATH = "/latest/meta-data/iam/security-credentials/";
- private static final String PROFILE_NAME = "some-profile";
- private static final String TOKEN_STUB = "some-token";
- private static final EnvironmentVariableHelper environmentVariableHelper = new EnvironmentVariableHelper();
-
- @RegisterExtension
- static WireMockExtension wireMockServer = WireMockExtension.newInstance()
- .options(wireMockConfig().dynamicPort().dynamicPort())
- .configureStaticDsl(true)
- .build();
-
- @BeforeEach
- void setup() {
- environmentVariableHelper.reset();
- environmentVariableHelper.set(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT,
- "http://localhost:" + wireMockServer.getPort());
- }
-
- @AfterAll
- static void teardown() {
- environmentVariableHelper.reset();
- }
-
- // ==================== Chain Propagation Tests ====================
-
- @Test
- void invalidate_chainPropagatesTo_allChildren() {
- TrackingCredentialsProvider provider1 = new TrackingCredentialsProvider("key1", "secret1");
- TrackingCredentialsProvider provider2 = new TrackingCredentialsProvider("key2", "secret2");
- TrackingCredentialsProvider provider3 = new TrackingCredentialsProvider("key3", "secret3");
-
- AwsCredentialsProviderChain chain = AwsCredentialsProviderChain.builder()
- .credentialsProviders(provider1, provider2, provider3)
- .build();
-
- AwsCredentialsIdentity identity = AwsBasicCredentials.create("key1", "secret1");
- chain.invalidate(identity);
-
- assertThat(provider1.invalidateCallCount).isEqualTo(1);
- assertThat(provider2.invalidateCallCount).isEqualTo(1);
- assertThat(provider3.invalidateCallCount).isEqualTo(1);
- }
-
- @SuppressWarnings("unchecked")
- @Test
- void invalidate_chainDoesNotShortCircuit_whenChildThrows() {
- AwsCredentialsProvider mockProvider1 = mock(AwsCredentialsProvider.class);
- AwsCredentialsProvider mockProvider2 = mock(AwsCredentialsProvider.class);
- AwsCredentialsProvider mockProvider3 = mock(AwsCredentialsProvider.class);
-
- // First provider throws on invalidate
- doThrow(new RuntimeException("Provider 1 failed")).when(mockProvider1).invalidate(any());
-
- AwsCredentialsProviderChain chain = AwsCredentialsProviderChain.builder()
- .credentialsProviders(mockProvider1, mockProvider2, mockProvider3)
- .build();
-
- AwsCredentialsIdentity identity = AwsBasicCredentials.create("key1", "secret1");
- chain.invalidate(identity);
-
- // All providers should still be called despite the first one throwing
- verify(mockProvider1, times(1)).invalidate(identity);
- verify(mockProvider2, times(1)).invalidate(identity);
- verify(mockProvider3, times(1)).invalidate(identity);
- }
-
- @Test
- void invalidate_chainWithStaticProvider_isNoOpWithoutError() {
- StaticCredentialsProvider staticProvider = StaticCredentialsProvider.create(
- AwsBasicCredentials.create("staticKey", "staticSecret"));
- TrackingCredentialsProvider trackingProvider = new TrackingCredentialsProvider("key1", "secret1");
-
- AwsCredentialsProviderChain chain = AwsCredentialsProviderChain.builder()
- .credentialsProviders(staticProvider, trackingProvider)
- .build();
-
- AwsCredentialsIdentity identity = AwsBasicCredentials.create("key1", "secret1");
-
- // Should not throw - StaticCredentialsProvider uses default no-op
- chain.invalidate(identity);
-
- // The tracking provider should still receive the call
- assertThat(trackingProvider.invalidateCallCount).isEqualTo(1);
- }
-
- // ==================== LazyAwsCredentialsProvider Delegation Tests ====================
-
- @Test
- void invalidate_lazyProvider_initialized_delegatesToInner() {
- TrackingCredentialsProvider innerProvider = new TrackingCredentialsProvider("key1", "secret1");
- LazyAwsCredentialsProvider lazyProvider = LazyAwsCredentialsProvider.create(() -> innerProvider);
-
- // Force initialization by calling resolveCredentials
- lazyProvider.resolveCredentials();
-
- AwsCredentialsIdentity identity = AwsBasicCredentials.create("key1", "secret1");
- lazyProvider.invalidate(identity);
-
- assertThat(innerProvider.invalidateCallCount).isEqualTo(1);
- }
-
- @Test
- void invalidate_lazyProvider_notInitialized_isNoOp() {
- List constructorCalled = new ArrayList<>();
- LazyAwsCredentialsProvider lazyProvider = LazyAwsCredentialsProvider.create(() -> {
- constructorCalled.add(true);
- return new TrackingCredentialsProvider("key1", "secret1");
- });
-
- // Do NOT call resolveCredentials — provider should not be initialized
- AwsCredentialsIdentity identity = AwsBasicCredentials.create("key1", "secret1");
- lazyProvider.invalidate(identity);
-
- // The supplier should never have been called - lazy not initialized
- assertThat(constructorCalled).isEmpty();
- }
-
- // ==================== DefaultCredentialsProvider Delegation Test ====================
-
- @Test
- void invalidate_defaultCredentialsProvider_delegatesToLazyChain() {
- // DefaultCredentialsProvider delegates to LazyAwsCredentialsProvider which delegates to chain.
- // We verify end-to-end by using profile credentials and checking delegation completes without error.
- DefaultCredentialsProvider provider = DefaultCredentialsProvider.builder()
- .profileFile(profileWithCredentials("testKey", "testSecret"))
- .profileName("test")
- .build();
-
- // Trigger initialization
- AwsCredentials credentials = provider.resolveCredentials();
- assertThat(credentials.accessKeyId()).isEqualTo("testKey");
-
- // Calling invalidate should not throw — it propagates through lazy -> chain -> children
- AwsCredentialsIdentity identity = AwsBasicCredentials.create("testKey", "testSecret");
- provider.invalidate(identity);
- }
-
- // ==================== Individual Provider AccessKeyId Matching Tests ====================
-
- @Test
- void invalidate_instanceProfileProvider_matchingAccessKeyId_invalidatesCache() {
- String accessKeyId = "ACCESS_KEY_ID";
- String expirationFarFuture = DateUtils.formatIso8601Date(Instant.now().plus(Duration.ofDays(1)));
- String credentialsJson = "{\"AccessKeyId\":\"" + accessKeyId + "\","
- + "\"SecretAccessKey\":\"SECRET_ACCESS_KEY\","
- + "\"Expiration\":\"" + expirationFarFuture + "\"}";
-
- stubImdsCredentials(credentialsJson);
-
- InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build();
-
- // First call: fetches and caches credentials
- AwsCredentials firstCredentials = provider.resolveCredentials();
- assertThat(firstCredentials.accessKeyId()).isEqualTo(accessKeyId);
-
- // Set up a different credential response for second fetch
- String newAccessKeyId = "NEW_ACCESS_KEY_ID";
- String newCredentialsJson = "{\"AccessKeyId\":\"" + newAccessKeyId + "\","
- + "\"SecretAccessKey\":\"NEW_SECRET_ACCESS_KEY\","
- + "\"Expiration\":\"" + expirationFarFuture + "\"}";
- stubImdsCredentials(newCredentialsJson);
-
- // Invalidate with matching accessKeyId — should mark cache as stale
- AwsCredentialsIdentity identity = AwsBasicCredentials.create(accessKeyId, "SECRET_ACCESS_KEY");
- provider.invalidate(identity);
-
- // Next resolveCredentials() should attempt a fresh fetch
- AwsCredentials refreshedCredentials = provider.resolveCredentials();
- assertThat(refreshedCredentials.accessKeyId()).isEqualTo(newAccessKeyId);
- }
-
- @Test
- void invalidate_instanceProfileProvider_nonMatchingAccessKeyId_doesNotInvalidateCache() {
- String accessKeyId = "ACCESS_KEY_ID";
- String expirationFarFuture = DateUtils.formatIso8601Date(Instant.now().plus(Duration.ofDays(1)));
- String credentialsJson = "{\"AccessKeyId\":\"" + accessKeyId + "\","
- + "\"SecretAccessKey\":\"SECRET_ACCESS_KEY\","
- + "\"Expiration\":\"" + expirationFarFuture + "\"}";
-
- stubImdsCredentials(credentialsJson);
-
- InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build();
-
- // First call: fetches and caches credentials
- AwsCredentials firstCredentials = provider.resolveCredentials();
- assertThat(firstCredentials.accessKeyId()).isEqualTo(accessKeyId);
-
- // Set up different credentials on the server (would be returned if cache is invalidated)
- String newCredentialsJson = "{\"AccessKeyId\":\"NEW_ACCESS_KEY_ID\","
- + "\"SecretAccessKey\":\"NEW_SECRET_ACCESS_KEY\","
- + "\"Expiration\":\"" + expirationFarFuture + "\"}";
- stubImdsCredentials(newCredentialsJson);
-
- // Invalidate with a DIFFERENT accessKeyId — should NOT invalidate cache
- AwsCredentialsIdentity differentIdentity = AwsBasicCredentials.create("DIFFERENT_KEY", "SECRET");
- provider.invalidate(differentIdentity);
-
- // Cache should still return the original cached credential
- AwsCredentials secondCredentials = provider.resolveCredentials();
- assertThat(secondCredentials.accessKeyId()).isEqualTo(accessKeyId);
- }
-
- // ==================== Helper Methods ====================
-
- private void stubImdsCredentials(String credentialsJson) {
- wireMockServer.stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH))
- .willReturn(aResponse().withBody(TOKEN_STUB)));
- wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH))
- .willReturn(aResponse().withBody(PROFILE_NAME)));
- wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + PROFILE_NAME))
- .willReturn(aResponse().withBody(credentialsJson)));
- }
-
- private java.util.function.Supplier profileWithCredentials(
- String accessKeyId, String secretAccessKey) {
- String contents = String.format("[test]\naws_access_key_id = %s\naws_secret_access_key = %s\n",
- accessKeyId, secretAccessKey);
- return () -> software.amazon.awssdk.profiles.ProfileFile.builder()
- .content(new software.amazon.awssdk.utils.StringInputStream(contents))
- .type(software.amazon.awssdk.profiles.ProfileFile.Type.CREDENTIALS)
- .build();
- }
-
- /**
- * A simple AwsCredentialsProvider that tracks invalidate() calls.
- */
- private static final class TrackingCredentialsProvider implements AwsCredentialsProvider {
- private final AwsBasicCredentials credentials;
- int invalidateCallCount = 0;
-
- TrackingCredentialsProvider(String accessKeyId, String secretAccessKey) {
- this.credentials = AwsBasicCredentials.create(accessKeyId, secretAccessKey);
- }
-
- @Override
- public AwsCredentials resolveCredentials() {
- return credentials;
- }
-
- @Override
- public CompletableFuture invalidate(AwsCredentialsIdentity identity) {
- invalidateCallCount++;
- return CompletableFuture.completedFuture(null);
- }
- }
-}
diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderTest.java
index 9a5ad4079e65..1b91407efb90 100644
--- a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderTest.java
+++ b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderTest.java
@@ -776,6 +776,61 @@ public Instant instant() {
}
}
+ @Test
+ void invalidate_matchingAccessKeyId_invalidatesCache() {
+ String accessKeyId = "ACCESS_KEY_ID";
+ String expirationFarFuture = DateUtils.formatIso8601Date(Instant.now().plus(Duration.ofDays(1)));
+ String credentialsJson = "{\"AccessKeyId\":\"" + accessKeyId + "\","
+ + "\"SecretAccessKey\":\"SECRET_ACCESS_KEY\","
+ + "\"Expiration\":\"" + expirationFarFuture + "\"}";
+
+ stubSecureCredentialsResponse(aResponse().withBody(credentialsJson));
+
+ InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build();
+
+ AwsCredentials firstCredentials = provider.resolveCredentials();
+ assertThat(firstCredentials.accessKeyId()).isEqualTo(accessKeyId);
+
+ String newAccessKeyId = "NEW_ACCESS_KEY_ID";
+ String newCredentialsJson = "{\"AccessKeyId\":\"" + newAccessKeyId + "\","
+ + "\"SecretAccessKey\":\"NEW_SECRET_ACCESS_KEY\","
+ + "\"Expiration\":\"" + expirationFarFuture + "\"}";
+ stubSecureCredentialsResponse(aResponse().withBody(newCredentialsJson));
+
+ AwsCredentialsIdentity identity = AwsBasicCredentials.create(accessKeyId, "SECRET_ACCESS_KEY");
+ provider.invalidate(identity).join();
+
+ AwsCredentials refreshedCredentials = provider.resolveCredentials();
+ assertThat(refreshedCredentials.accessKeyId()).isEqualTo(newAccessKeyId);
+ }
+
+ @Test
+ void invalidate_nonMatchingAccessKeyId_doesNotInvalidateCache() {
+ String accessKeyId = "ACCESS_KEY_ID";
+ String expirationFarFuture = DateUtils.formatIso8601Date(Instant.now().plus(Duration.ofDays(1)));
+ String credentialsJson = "{\"AccessKeyId\":\"" + accessKeyId + "\","
+ + "\"SecretAccessKey\":\"SECRET_ACCESS_KEY\","
+ + "\"Expiration\":\"" + expirationFarFuture + "\"}";
+
+ stubSecureCredentialsResponse(aResponse().withBody(credentialsJson));
+
+ InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build();
+
+ AwsCredentials firstCredentials = provider.resolveCredentials();
+ assertThat(firstCredentials.accessKeyId()).isEqualTo(accessKeyId);
+
+ String newCredentialsJson = "{\"AccessKeyId\":\"NEW_ACCESS_KEY_ID\","
+ + "\"SecretAccessKey\":\"NEW_SECRET_ACCESS_KEY\","
+ + "\"Expiration\":\"" + expirationFarFuture + "\"}";
+ stubSecureCredentialsResponse(aResponse().withBody(newCredentialsJson));
+
+ AwsCredentialsIdentity differentIdentity = AwsBasicCredentials.create("DIFFERENT_KEY", "SECRET");
+ provider.invalidate(differentIdentity).join();
+
+ AwsCredentials secondCredentials = provider.resolveCredentials();
+ assertThat(secondCredentials.accessKeyId()).isEqualTo(accessKeyId);
+ }
+
private static ProfileFileSupplier supply(Iterable iterable) {
return iterable.iterator()::next;
}
diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/internal/LazyAwsCredentialsProviderTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/internal/LazyAwsCredentialsProviderTest.java
index 43ffcd9cd28e..6d8ed70992ad 100644
--- a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/internal/LazyAwsCredentialsProviderTest.java
+++ b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/internal/LazyAwsCredentialsProviderTest.java
@@ -15,11 +15,13 @@
package software.amazon.awssdk.auth.credentials.internal;
+import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity;
import software.amazon.awssdk.utils.SdkAutoCloseable;
public class LazyAwsCredentialsProviderTest {
@@ -75,4 +77,27 @@ public void delegatesClosesInitializerEvenIfGetFails() {
private interface CloseableSupplier extends Supplier, SdkAutoCloseable {}
private interface CloseableCredentialsProvider extends SdkAutoCloseable, AwsCredentialsProvider {}
+
+ @Test
+ public void invalidate_whenInitialized_delegatesToInner() {
+ LazyAwsCredentialsProvider credentialsProvider = LazyAwsCredentialsProvider.create(credentialsConstructor);
+ credentialsProvider.resolveCredentials();
+
+ AwsCredentialsIdentity identity = Mockito.mock(AwsCredentialsIdentity.class);
+ Mockito.when(credentials.invalidate(identity)).thenReturn(CompletableFuture.completedFuture(null));
+
+ credentialsProvider.invalidate(identity);
+
+ Mockito.verify(credentials).invalidate(identity);
+ }
+
+ @Test
+ public void invalidate_whenNotInitialized_doesNotInvokeSupplier() {
+ LazyAwsCredentialsProvider credentialsProvider = LazyAwsCredentialsProvider.create(credentialsConstructor);
+
+ AwsCredentialsIdentity identity = Mockito.mock(AwsCredentialsIdentity.class);
+ credentialsProvider.invalidate(identity);
+
+ Mockito.verifyNoMoreInteractions(credentialsConstructor);
+ }
}
diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsErrorCode.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsErrorCode.java
index 4a31dbd985d3..6fd9267b8000 100644
--- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsErrorCode.java
+++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsErrorCode.java
@@ -71,7 +71,6 @@ public final class AwsErrorCode {
Set authErrorCodes = new HashSet<>(3);
authErrorCodes.add("ExpiredToken");
authErrorCodes.add("InvalidToken");
- authErrorCodes.add("AuthFailure");
AUTH_ERROR_CODES = unmodifiableSet(authErrorCodes);
}
diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/exception/SdkServiceException.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/exception/SdkServiceException.java
index 2f5cc422a346..fc3a339cded6 100644
--- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/exception/SdkServiceException.java
+++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/exception/SdkServiceException.java
@@ -103,9 +103,9 @@ public boolean isRetryableException() {
}
/**
- * Specifies whether an exception indicates an authentication or authorization error where the credentials
- * used for the request were rejected by the service. Subclasses can override this to check service-specific
- * error codes (e.g., {@code ExpiredToken}, {@code InvalidToken}, {@code AuthFailure}).
+ * Specifies whether an exception indicates an authentication error where the credentials used for the request
+ * were rejected because they are invalid or expired. This method must return {@code false} for authorization
+ * errors such as {@code AccessDenied}, where the credentials are valid but lack permission for the requested action.
*
* When this returns {@code true}, the SDK may invalidate cached credentials so that the next attempt
* resolves fresh ones.
diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/AuthErrorInvalidationHelper.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/AuthErrorInvalidationHelper.java
index 2894323dcf92..d5fe21a707b9 100644
--- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/AuthErrorInvalidationHelper.java
+++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/AuthErrorInvalidationHelper.java
@@ -81,6 +81,10 @@ public static void invalidateIfAuthError(Throwable exception, RequestExecutionCo
private static void doInvalidate(SelectedAuthScheme selectedAuthScheme) {
T resolvedIdentity = CompletableFutureUtils.joinLikeSync(selectedAuthScheme.identity());
IdentityProvider provider = selectedAuthScheme.identityProvider();
- provider.invalidate(resolvedIdentity);
+ try {
+ CompletableFutureUtils.joinLikeSync(provider.invalidate(resolvedIdentity));
+ } catch (Exception e) {
+ LOG.debug(() -> "Failed to invalidate identity provider: " + e.getMessage(), e);
+ }
}
}
diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/AuthErrorInvalidationHelperTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/AuthErrorInvalidationHelperTest.java
index 8c913effbf2b..fa8a2bb1b0c5 100644
--- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/AuthErrorInvalidationHelperTest.java
+++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/AuthErrorInvalidationHelperTest.java
@@ -51,65 +51,53 @@
public class AuthErrorInvalidationHelperTest {
@ParameterizedTest
- @ValueSource(strings = {"ExpiredToken", "InvalidToken", "AuthFailure"})
+ @ValueSource(strings = {"ExpiredToken", "InvalidToken"})
void invalidateIfAuthError_whenAuthErrorCode_triggersInvalidation(String errorCode) {
- // Arrange
TrackingIdentityProvider provider = new TrackingIdentityProvider();
TestIdentity identity = new TestIdentity();
RequestExecutionContext context = contextWithProvider(provider, identity);
Throwable exception = serviceExceptionWithErrorCode(errorCode);
- // Act
AuthErrorInvalidationHelper.invalidateIfAuthError(exception, context);
- // Assert
assertThat(provider.invalidateCalled()).isTrue();
assertThat(provider.lastInvalidatedIdentity()).isSameAs(identity);
}
@Test
void invalidateIfAuthError_whenAccessDenied_doesNotTriggerInvalidation() {
- // Arrange
TrackingIdentityProvider provider = new TrackingIdentityProvider();
TestIdentity identity = new TestIdentity();
RequestExecutionContext context = contextWithProvider(provider, identity);
Throwable exception = serviceExceptionWithErrorCode("AccessDenied");
- // Act
AuthErrorInvalidationHelper.invalidateIfAuthError(exception, context);
- // Assert
assertThat(provider.invalidateCalled()).isFalse();
}
@ParameterizedTest
- @ValueSource(strings = {"ThrottlingException", "InternalServerError", "ValidationException", "ResourceNotFoundException"})
+ @ValueSource(strings = {"ThrottlingException", "InternalServerError", "ValidationException", "ResourceNotFoundException", "AuthFailure"})
void invalidateIfAuthError_whenUnknownErrorCode_doesNotTriggerInvalidation(String errorCode) {
- // Arrange
TrackingIdentityProvider provider = new TrackingIdentityProvider();
TestIdentity identity = new TestIdentity();
RequestExecutionContext context = contextWithProvider(provider, identity);
Throwable exception = serviceExceptionWithErrorCode(errorCode);
- // Act
AuthErrorInvalidationHelper.invalidateIfAuthError(exception, context);
- // Assert
assertThat(provider.invalidateCalled()).isFalse();
}
@ParameterizedTest
@MethodSource("nonServiceExceptions")
void invalidateIfAuthError_whenNonSdkServiceException_doesNotTriggerInvalidation(Throwable exception) {
- // Arrange
TrackingIdentityProvider provider = new TrackingIdentityProvider();
TestIdentity identity = new TestIdentity();
RequestExecutionContext context = contextWithProvider(provider, identity);
- // Act
AuthErrorInvalidationHelper.invalidateIfAuthError(exception, context);
- // Assert
assertThat(provider.invalidateCalled()).isFalse();
}
@@ -123,11 +111,9 @@ static Stream nonServiceExceptions() {
@Test
void invalidateIfAuthError_whenSelectedAuthSchemeIsNull_doesNotThrow() {
- // Arrange — context with no SELECTED_AUTH_SCHEME attribute
RequestExecutionContext context = contextWithNoAuthScheme();
Throwable exception = serviceExceptionWithErrorCode("ExpiredToken");
- // Act & Assert — no NPE, no invalidation
assertThatNoException().isThrownBy(() ->
AuthErrorInvalidationHelper.invalidateIfAuthError(exception, context)
);
@@ -135,7 +121,6 @@ void invalidateIfAuthError_whenSelectedAuthSchemeIsNull_doesNotThrow() {
@Test
void invalidateIfAuthError_whenIdentityProviderIsNull_doesNotThrow() {
- // Arrange — SelectedAuthScheme created with 3-arg constructor (null identityProvider)
TestIdentity identity = new TestIdentity();
SelectedAuthScheme selectedAuthScheme = new SelectedAuthScheme<>(
CompletableFuture.completedFuture(identity),
@@ -146,7 +131,6 @@ void invalidateIfAuthError_whenIdentityProviderIsNull_doesNotThrow() {
RequestExecutionContext context = contextWithSelectedAuthScheme(selectedAuthScheme);
Throwable exception = serviceExceptionWithErrorCode("ExpiredToken");
- // Act & Assert — no NPE, no invalidation
assertThatNoException().isThrownBy(() ->
AuthErrorInvalidationHelper.invalidateIfAuthError(exception, context)
);
@@ -154,7 +138,6 @@ void invalidateIfAuthError_whenIdentityProviderIsNull_doesNotThrow() {
@Test
void invalidateIfAuthError_whenInvalidateThrowsException_doesNotPropagate() {
- // Arrange — provider that throws on invalidate()
ThrowingIdentityProvider provider = new ThrowingIdentityProvider();
TestIdentity identity = new TestIdentity();
SelectedAuthScheme selectedAuthScheme = new SelectedAuthScheme<>(
@@ -167,7 +150,6 @@ void invalidateIfAuthError_whenInvalidateThrowsException_doesNotPropagate() {
RequestExecutionContext context = contextWithSelectedAuthScheme(selectedAuthScheme);
Throwable exception = serviceExceptionWithErrorCode("ExpiredToken");
- // Act & Assert — exception is caught internally, method returns normally
assertThatNoException().isThrownBy(() ->
AuthErrorInvalidationHelper.invalidateIfAuthError(exception, context)
);
@@ -293,7 +275,7 @@ public CompletableFuture invalidate(TestIdentity identity) {
*/
private static class TestAwsServiceException extends SdkServiceException {
private static final Set AUTH_ERROR_CODES = new HashSet<>(Arrays.asList(
- "ExpiredToken", "InvalidToken", "AuthFailure"
+ "ExpiredToken", "InvalidToken"
));
private final String errorCode;
diff --git a/services/signin/src/main/java/software/amazon/awssdk/services/signin/auth/LoginCredentialsProvider.java b/services/signin/src/main/java/software/amazon/awssdk/services/signin/auth/LoginCredentialsProvider.java
index 9650a787303f..e90986badc5b 100644
--- a/services/signin/src/main/java/software/amazon/awssdk/services/signin/auth/LoginCredentialsProvider.java
+++ b/services/signin/src/main/java/software/amazon/awssdk/services/signin/auth/LoginCredentialsProvider.java
@@ -299,16 +299,10 @@ public AwsCredentials resolveCredentials() {
@Override
public CompletableFuture invalidate(AwsCredentialsIdentity identity) {
- if (identity instanceof AwsCredentialsIdentity) {
- String rejectedAccessKeyId = identity.accessKeyId();
- credentialCache.invalidate(cachedCreds -> {
- if (cachedCreds instanceof AwsCredentialsIdentity) {
- return rejectedAccessKeyId.equals(((AwsCredentialsIdentity) cachedCreds).accessKeyId());
- }
- return false;
- });
- }
- return CompletableFuture.completedFuture(null);
+ String rejectedAccessKeyId = identity.accessKeyId();
+ return CompletableFuture.runAsync(() ->
+ credentialCache.invalidate(cachedCreds -> rejectedAccessKeyId.equals(cachedCreds.accessKeyId()))
+ );
}
@Override
diff --git a/services/sso/src/main/java/software/amazon/awssdk/services/sso/auth/SsoCredentialsProvider.java b/services/sso/src/main/java/software/amazon/awssdk/services/sso/auth/SsoCredentialsProvider.java
index f133a0531652..8e22e0853aa2 100644
--- a/services/sso/src/main/java/software/amazon/awssdk/services/sso/auth/SsoCredentialsProvider.java
+++ b/services/sso/src/main/java/software/amazon/awssdk/services/sso/auth/SsoCredentialsProvider.java
@@ -28,7 +28,6 @@
import software.amazon.awssdk.auth.credentials.AwsCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
-import software.amazon.awssdk.auth.credentials.internal.CredentialsInvalidationUtils;
import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId;
import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity;
import software.amazon.awssdk.services.sso.SsoClient;
@@ -173,8 +172,10 @@ public AwsCredentials resolveCredentials() {
@Override
public CompletableFuture invalidate(AwsCredentialsIdentity identity) {
- return CredentialsInvalidationUtils.invalidateCredentialsCache(
- identity, credentialCache, holder -> holder.sessionCredentials().accessKeyId());
+ String rejectedAccessKeyId = identity.accessKeyId();
+ return CompletableFuture.runAsync(() ->
+ credentialCache.invalidate(holder -> rejectedAccessKeyId.equals(holder.sessionCredentials().accessKeyId()))
+ );
}
@Override
diff --git a/services/sso/src/test/java/software/amazon/awssdk/services/sso/auth/SsoCredentialsProviderTest.java b/services/sso/src/test/java/software/amazon/awssdk/services/sso/auth/SsoCredentialsProviderTest.java
index fc0668a10811..dc0f54193e33 100644
--- a/services/sso/src/test/java/software/amazon/awssdk/services/sso/auth/SsoCredentialsProviderTest.java
+++ b/services/sso/src/test/java/software/amazon/awssdk/services/sso/auth/SsoCredentialsProviderTest.java
@@ -27,9 +27,11 @@
import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId;
+import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity;
import software.amazon.awssdk.services.sso.SsoClient;
import software.amazon.awssdk.services.sso.model.GetRoleCredentialsRequest;
import software.amazon.awssdk.services.sso.model.GetRoleCredentialsResponse;
@@ -209,6 +211,73 @@ public void noCachedCredentials_anyFailure_throwsImmediately() {
+ @Test
+ public void invalidate_matchingAccessKeyId_causesRefresh() {
+ ssoClient = mock(SsoClient.class);
+ RoleCredentials credentials = RoleCredentials.builder()
+ .accessKeyId("a")
+ .secretAccessKey("b")
+ .sessionToken("c")
+ .expiration(Instant.now().plus(Duration.ofHours(5)).toEpochMilli())
+ .build();
+ RoleCredentials credentials2 = RoleCredentials.builder()
+ .accessKeyId("x")
+ .secretAccessKey("y")
+ .sessionToken("z")
+ .expiration(Instant.now().plus(Duration.ofHours(5)).toEpochMilli())
+ .build();
+
+ Supplier supplier = getRequestSupplier();
+ when(ssoClient.getRoleCredentials(supplier.get()))
+ .thenReturn(getResponse(credentials))
+ .thenReturn(getResponse(credentials2));
+
+ try (SsoCredentialsProvider credentialsProvider = SsoCredentialsProvider.builder()
+ .refreshRequest(supplier)
+ .ssoClient(ssoClient)
+ .build()) {
+ AwsSessionCredentials first = (AwsSessionCredentials) credentialsProvider.resolveCredentials();
+ assertThat(first.accessKeyId()).isEqualTo("a");
+
+ AwsCredentialsIdentity identity = AwsBasicCredentials.create("a", "b");
+ credentialsProvider.invalidate(identity).join();
+
+ AwsSessionCredentials second = (AwsSessionCredentials) credentialsProvider.resolveCredentials();
+ assertThat(second.accessKeyId()).isEqualTo("x");
+ }
+ }
+
+ @Test
+ public void invalidate_nonMatchingAccessKeyId_doesNotCauseRefresh() {
+ ssoClient = mock(SsoClient.class);
+ RoleCredentials credentials = RoleCredentials.builder()
+ .accessKeyId("a")
+ .secretAccessKey("b")
+ .sessionToken("c")
+ .expiration(Instant.now().plus(Duration.ofHours(5)).toEpochMilli())
+ .build();
+
+ Supplier supplier = getRequestSupplier();
+ when(ssoClient.getRoleCredentials(supplier.get())).thenReturn(getResponse(credentials));
+
+ try (SsoCredentialsProvider credentialsProvider = SsoCredentialsProvider.builder()
+ .refreshRequest(supplier)
+ .ssoClient(ssoClient)
+ .build()) {
+ AwsSessionCredentials first = (AwsSessionCredentials) credentialsProvider.resolveCredentials();
+ assertThat(first.accessKeyId()).isEqualTo("a");
+
+ AwsCredentialsIdentity identity = AwsBasicCredentials.create("different-key", "b");
+ credentialsProvider.invalidate(identity).join();
+
+ AwsSessionCredentials second = (AwsSessionCredentials) credentialsProvider.resolveCredentials();
+ assertThat(second.accessKeyId()).isEqualTo("a");
+ callClient(verify(ssoClient, times(1)), Mockito.any());
+ }
+ }
+
+
+
private GetRoleCredentialsRequestSupplier getRequestSupplier() {
return new GetRoleCredentialsRequestSupplier(GetRoleCredentialsRequest.builder()
.accountId("123456789")
diff --git a/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsCredentialsProvider.java b/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsCredentialsProvider.java
index 8fd1acda683d..2782ec960986 100644
--- a/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsCredentialsProvider.java
+++ b/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsCredentialsProvider.java
@@ -26,7 +26,6 @@
import software.amazon.awssdk.auth.credentials.AwsCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
-import software.amazon.awssdk.auth.credentials.internal.CredentialsInvalidationUtils;
import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity;
import software.amazon.awssdk.services.sts.StsClient;
import software.amazon.awssdk.utils.Logger;
@@ -128,8 +127,10 @@ public void close() {
@Override
public CompletableFuture invalidate(AwsCredentialsIdentity identity) {
- return CredentialsInvalidationUtils.invalidateCredentialsCache(
- identity, sessionCache, AwsCredentialsIdentity::accessKeyId);
+ String rejectedAccessKeyId = identity.accessKeyId();
+ return CompletableFuture.runAsync(() ->
+ sessionCache.invalidate(cachedCreds -> rejectedAccessKeyId.equals(cachedCreds.accessKeyId()))
+ );
}
/**
diff --git a/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsCredentialsProviderTestBase.java b/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsCredentialsProviderTestBase.java
index 10014ddbf7c9..d6849d804dc4 100644
--- a/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsCredentialsProviderTestBase.java
+++ b/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsCredentialsProviderTestBase.java
@@ -28,9 +28,11 @@
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentials;
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
import software.amazon.awssdk.core.exception.SdkClientException;
+import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity;
import software.amazon.awssdk.services.sts.StsClient;
import software.amazon.awssdk.services.sts.endpoints.internal.Arn;
import software.amazon.awssdk.services.sts.model.Credentials;
@@ -159,6 +161,55 @@ public void initialFetchFailureThrowsException_noCachedCredentials() {
}
}
+ @Test
+ public void invalidate_matchingAccessKeyId_causesRefresh() {
+ Credentials credentials = Credentials.builder()
+ .accessKeyId("a").secretAccessKey("b").sessionToken("c")
+ .expiration(Instant.now().plus(Duration.ofHours(5)))
+ .build();
+ Credentials credentials2 = Credentials.builder()
+ .accessKeyId("x").secretAccessKey("y").sessionToken("z")
+ .expiration(Instant.now().plus(Duration.ofHours(5)))
+ .build();
+ RequestT request = getRequest();
+ when(callClient(stsClient, request))
+ .thenReturn(getResponse(credentials))
+ .thenReturn(getResponse(credentials2));
+
+ try (StsCredentialsProvider credentialsProvider = createCredentialsProviderBuilder(request).stsClient(stsClient).build()) {
+ AwsCredentials first = credentialsProvider.resolveCredentials();
+ assertThat(first.accessKeyId()).isEqualTo("a");
+
+ AwsCredentialsIdentity identity = AwsBasicCredentials.create("a", "b");
+ credentialsProvider.invalidate(identity).join();
+
+ AwsCredentials second = credentialsProvider.resolveCredentials();
+ assertThat(second.accessKeyId()).isEqualTo("x");
+ }
+ }
+
+ @Test
+ public void invalidate_nonMatchingAccessKeyId_doesNotCauseRefresh() {
+ Credentials credentials = Credentials.builder()
+ .accessKeyId("a").secretAccessKey("b").sessionToken("c")
+ .expiration(Instant.now().plus(Duration.ofHours(5)))
+ .build();
+ RequestT request = getRequest();
+ when(callClient(stsClient, request)).thenReturn(getResponse(credentials));
+
+ try (StsCredentialsProvider credentialsProvider = createCredentialsProviderBuilder(request).stsClient(stsClient).build()) {
+ AwsCredentials first = credentialsProvider.resolveCredentials();
+ assertThat(first.accessKeyId()).isEqualTo("a");
+
+ AwsCredentialsIdentity identity = AwsBasicCredentials.create("different", "b");
+ credentialsProvider.invalidate(identity).join();
+
+ AwsCredentials second = credentialsProvider.resolveCredentials();
+ assertThat(second.accessKeyId()).isEqualTo("a");
+ callClient(verify(stsClient, times(1)), Mockito.any());
+ }
+ }
+
public void callClientWithCredentialsProvider(Instant credentialsExpirationDate, int numTimesInvokeCredentialsProvider, boolean overrideStaleAndPrefetchTimes) {
Credentials credentials = Credentials.builder().accessKeyId("a").secretAccessKey("b").sessionToken("c").expiration(credentialsExpirationDate).build();
RequestT request = getRequest();
diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/retry/AuthErrorInvalidationIntegrationTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/retry/AuthErrorInvalidationIntegrationTest.java
index 57cae1dd3f4c..a555c5d4cda8 100644
--- a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/retry/AuthErrorInvalidationIntegrationTest.java
+++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/retry/AuthErrorInvalidationIntegrationTest.java
@@ -16,6 +16,7 @@
package software.amazon.awssdk.services.retry;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.net.URI;
import java.time.Clock;
@@ -31,7 +32,7 @@
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
-import software.amazon.awssdk.core.retry.RetryMode;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
import software.amazon.awssdk.http.AbortableInputStream;
import software.amazon.awssdk.http.HttpExecuteResponse;
import software.amazon.awssdk.http.SdkHttpRequest;
@@ -47,29 +48,25 @@
/**
* Integration test verifying the end-to-end flow:
- * service returns ExpiredToken → credentials invalidated → retry uses fresh credentials.
+ * service returns ExpiredToken → credentials invalidated → next call uses fresh credentials.
*
* Also verifies backoff interaction: when refresh backoff is active, invalidate() does not
* bypass it, and stale cached credentials are returned until the backoff elapses.
- *
- * Validates Requirements: 6, 8, 9, 10, 21
*/
public class AuthErrorInvalidationIntegrationTest {
/**
* End-to-end: ExpiredToken → invalidation is triggered → next call uses fresh credentials.
*
- * The SDK resolves identity once per API call (in beforeExecution interceptor), so the retry
- * within the same call reuses the same identity. However, invalidation marks the provider's
- * cache as stale, ensuring the NEXT API call resolves fresh credentials.
+ * ExpiredToken exceptions are not retryable, so the first API call fails immediately.
+ * However, invalidation is still triggered on the provider, ensuring the next API call
+ * resolves fresh credentials.
*
* Scenario:
- * 1. First API call: signed with "old-key", service returns 500 with ExpiredToken
+ * 1. First API call: signed with "old-key", service returns ExpiredToken (not retryable)
* 2. SDK calls invalidate() on the provider (switching it to return "new-key")
- * 3. SDK retries (same identity — "old-key" — because identity was already resolved)
- * 4. Second retry succeeds with 200
- * 5. Second API call: resolves fresh identity "new-key"
- * 6. Verify invalidate was called and second API call uses new credentials
+ * 3. First call fails with ExpiredToken exception
+ * 4. Second API call: resolves fresh identity "new-key", succeeds
*/
@Test
public void expiredToken_invalidatesCredentials_nextCallUsesFreshCredentials() {
@@ -82,24 +79,23 @@ public void expiredToken_invalidatesCredentials_nextCallUsesFreshCredentials() {
.region(Region.US_EAST_1)
.endpointOverride(URI.create("http://localhost"))
.httpClient(mockHttpClient)
- .overrideConfiguration(c -> c.retryStrategy(RetryMode.STANDARD))
.build()) {
- // First API call: 500 ExpiredToken (retried), then 200 on retry
- mockHttpClient.stubResponses(expiredTokenResponse(), successResponse());
+ // First API call: ExpiredToken (not retryable — fails immediately)
+ mockHttpClient.stubResponses(expiredTokenResponse());
- // First call succeeds on retry (same credentials used for both attempts since
- // identity is resolved once per API call)
- client.allTypes();
+ assertThatThrownBy(() -> client.allTypes())
+ .isInstanceOf(AwsServiceException.class)
+ .satisfies(e -> assertThat(((AwsServiceException) e).awsErrorDetails().errorCode())
+ .isEqualTo("ExpiredToken"));
// Verify invalidate was called during the first API call
assertThat(credentialsProvider.invalidateCallCount()).isEqualTo(1);
- // Verify first call's requests both used "old-key" (same identity, same call)
+ // Verify first call used "old-key"
List firstCallRequests = mockHttpClient.getRequests();
- assertThat(firstCallRequests).hasSize(2);
+ assertThat(firstCallRequests).hasSize(1);
assertRequestUsedAccessKey(firstCallRequests.get(0), "old-key");
- assertRequestUsedAccessKey(firstCallRequests.get(1), "old-key");
// Now make a second API call — this should resolve fresh credentials
mockHttpClient.reset();
@@ -115,13 +111,7 @@ public void expiredToken_invalidatesCredentials_nextCallUsesFreshCredentials() {
}
/**
- * Verify that AccessDenied does NOT trigger invalidation.
- *
- * Scenario:
- * 1. Client makes a request signed with "my-key"
- * 2. Service returns 403 with error code "AccessDenied"
- * 3. SDK does NOT invalidate credentials (AccessDenied is not an auth error for invalidation)
- * 4. Request fails (403 is not retryable by default for standard retry)
+ * Verify that AccessDenied does NOT trigger invalidation and the exception propagates.
*/
@Test
public void accessDenied_doesNotInvalidateCredentials() {
@@ -134,18 +124,14 @@ public void accessDenied_doesNotInvalidateCredentials() {
.region(Region.US_EAST_1)
.endpointOverride(URI.create("http://localhost"))
.httpClient(mockHttpClient)
- .overrideConfiguration(c -> c.retryStrategy(RetryMode.STANDARD))
.build()) {
- // Response: 403 with AccessDenied error code
- HttpExecuteResponse accessDeniedResponse = accessDeniedResponse();
- mockHttpClient.stubResponses(accessDeniedResponse);
+ mockHttpClient.stubResponses(accessDeniedResponse());
- try {
- client.allTypes();
- } catch (Exception e) {
- // Expected — 403 is not retryable
- }
+ assertThatThrownBy(() -> client.allTypes())
+ .isInstanceOf(AwsServiceException.class)
+ .satisfies(e -> assertThat(((AwsServiceException) e).awsErrorDetails().errorCode())
+ .isEqualTo("AccessDenied"));
// Verify invalidate was NOT called
assertThat(credentialsProvider.invalidateCallCount()).isEqualTo(0);
@@ -158,9 +144,7 @@ public void accessDenied_doesNotInvalidateCredentials() {
* This test verifies that when CachedSupplier has an active refresh backoff
* (nextAllowedRefreshTime in the future), calling invalidate() marks the cache stale
* but does NOT bypass the backoff. The next get() still returns cached credentials
- * until the backoff elapses.
- *
- * Validates Requirements: 9, 10
+ * until the backoff elapses, at which point fresh credentials are obtained.
*/
@Test
public void invalidation_duringActiveBackoff_returnsStaleCredentials() {
@@ -171,7 +155,6 @@ public void invalidation_duringActiveBackoff_returnsStaleCredentials() {
AtomicInteger fetchCount = new AtomicInteger(0);
AtomicReference>> supplierRef = new AtomicReference<>();
- // Set up the initial supplier that returns credentials
supplierRef.set(() -> RefreshResult.builder("access-key-1")
.staleTime(now.plusSeconds(60))
.prefetchTime(now.plusSeconds(30))
@@ -208,7 +191,6 @@ public void invalidation_duringActiveBackoff_returnsStaleCredentials() {
.build());
// get() should return STALE credentials because backoff is still active
- // (nextAllowedRefreshTime = ~now+61 + [300,600]s, we're at now+62)
assertThat(cache.get()).isEqualTo("access-key-1");
// Advance past maximum possible backoff (61 + 600 = 661 seconds from original now)
@@ -219,65 +201,6 @@ public void invalidation_duringActiveBackoff_returnsStaleCredentials() {
}
}
- /**
- * Verify that after backoff elapses following invalidation, fresh credentials are obtained.
- *
- * This tests the complete lifecycle:
- * 1. Cache has credentials
- * 2. Refresh fails → backoff is set
- * 3. invalidate() is called → staleTime set to now (but backoff stays)
- * 4. During backoff → stale value returned
- * 5. After backoff → fresh value obtained
- *
- * Validates Requirements: 8, 9, 10
- */
- @Test
- public void afterBackoffElapses_invalidatedCache_obtainsFreshCredentials() {
- AdjustableClock clock = new AdjustableClock();
- Instant now = Instant.parse("2024-01-01T00:00:00Z");
- clock.time = now;
-
- AtomicReference>> supplierRef = new AtomicReference<>();
- supplierRef.set(() -> RefreshResult.builder("cred-A")
- .staleTime(now.plusSeconds(60))
- .prefetchTime(now.plusSeconds(30))
- .build());
-
- try (CachedSupplier cache = CachedSupplier.builder(() -> supplierRef.get().get())
- .staleValueBehavior(CachedSupplier.StaleValueBehavior.ALLOW)
- .clock(clock)
- .build()) {
-
- // Initial fetch
- assertThat(cache.get()).isEqualTo("cred-A");
-
- // Advance time so cache is stale and trigger failure
- clock.time = now.plusSeconds(61);
- supplierRef.set(() -> {
- throw new RuntimeException("source down");
- });
- assertThat(cache.get()).isEqualTo("cred-A"); // stale value returned
-
- // Invalidate
- clock.time = now.plusSeconds(62);
- cache.invalidate(v -> v.equals("cred-A"));
-
- // Set up fresh supplier
- supplierRef.set(() -> RefreshResult.builder("cred-B")
- .staleTime(now.plusSeconds(7200))
- .prefetchTime(now.plusSeconds(5400))
- .build());
-
- // Still within backoff — stale returned
- clock.time = now.plusSeconds(100);
- assertThat(cache.get()).isEqualTo("cred-A");
-
- // Advance past max backoff
- clock.time = now.plusSeconds(700);
- assertThat(cache.get()).isEqualTo("cred-B");
- }
- }
-
// --- Helper methods ---
private void assertRequestUsedAccessKey(SdkHttpRequest request, String expectedAccessKeyId) {
@@ -291,13 +214,9 @@ private void assertRequestUsedAccessKey(SdkHttpRequest request, String expectedA
private HttpExecuteResponse expiredTokenResponse() {
String errorBody = "{\"__type\":\"ExpiredTokenException\",\"message\":\"The security token included in the request "
+ "is expired\"}";
- // Use 500 so the standard retry strategy retries it.
- // In production, services may return 4xx for expired tokens, but the SDK only retries
- // on 5xx status codes or specific retryable error codes. Using 500 ensures the retry
- // happens so we can verify the full invalidation → retry → fresh credentials flow.
return HttpExecuteResponse.builder()
.response(SdkHttpResponse.builder()
- .statusCode(500)
+ .statusCode(403)
.putHeader("x-amzn-ErrorType", "ExpiredToken")
.putHeader("content-length",
String.valueOf(errorBody.length()))
diff --git a/utils/src/main/java/software/amazon/awssdk/utils/cache/CachedSupplier.java b/utils/src/main/java/software/amazon/awssdk/utils/cache/CachedSupplier.java
index 889affec34d1..bd830c00e9e4 100644
--- a/utils/src/main/java/software/amazon/awssdk/utils/cache/CachedSupplier.java
+++ b/utils/src/main/java/software/amazon/awssdk/utils/cache/CachedSupplier.java
@@ -176,15 +176,22 @@ public T get() {
* This method MUST NOT discard the cached value.
* This method MUST NOT clear or modify {@code nextAllowedRefreshTime}.
*
+ * If there is no cached value, this method is a no-op — the predicate will not be called.
+ *
* @param matchesCachedValue A predicate that returns true if the cached value
- * is the one that should be invalidated.
+ * is the one that should be invalidated. The value passed
+ * to the predicate is guaranteed to be non-null.
*/
public void invalidate(Predicate matchesCachedValue) {
try {
boolean lockAcquired = refreshLock.tryLock(BLOCKING_REFRESH_MAX_WAIT.getSeconds(), TimeUnit.SECONDS);
+ if (!lockAcquired) {
+ log.debug(() -> "(" + cachedValueName + ") Unable to acquire lock for invalidation; skipping.");
+ return;
+ }
try {
RefreshResult currentCachedValue = this.cachedValue;
- if (currentCachedValue == null) {
+ if (currentCachedValue == null || currentCachedValue.value() == null) {
return;
}
if (!matchesCachedValue.test(currentCachedValue.value())) {
@@ -200,9 +207,7 @@ public void invalidate(Predicate matchesCachedValue) {
log.debug(() -> "(" + cachedValueName + ") Cached value invalidated. "
+ "Next get() will attempt mandatory refresh.");
} finally {
- if (lockAcquired) {
- refreshLock.unlock();
- }
+ refreshLock.unlock();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
diff --git a/utils/src/test/java/software/amazon/awssdk/utils/cache/CachedSupplierInvalidateTest.java b/utils/src/test/java/software/amazon/awssdk/utils/cache/CachedSupplierInvalidateTest.java
deleted file mode 100644
index 5ba8d494766d..000000000000
--- a/utils/src/test/java/software/amazon/awssdk/utils/cache/CachedSupplierInvalidateTest.java
+++ /dev/null
@@ -1,559 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed
- * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-package software.amazon.awssdk.utils.cache;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static software.amazon.awssdk.utils.cache.CachedSupplier.StaleValueBehavior.ALLOW;
-
-import java.time.Clock;
-import java.time.Instant;
-import java.time.ZoneId;
-import java.time.ZoneOffset;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.function.Supplier;
-import org.junit.jupiter.api.Test;
-
-/**
- * Unit tests for {@link CachedSupplier#invalidate} and the {@code nextAllowedRefreshTime} backoff behavior.
- *
- * Validates Requirements: 8, 9, 10, 12, 13
- */
-public class CachedSupplierInvalidateTest {
-
- // --- Test 1: Predicate matching — true case ---
-
- @Test
- public void invalidate_predicateMatches_setsStaleTimeToNow_triggersRefresh() {
- AdjustableClock clock = new AdjustableClock();
- MutableSupplier supplier = new MutableSupplier();
- Instant now = Instant.parse("2024-01-01T00:00:00Z");
- clock.time = now;
-
- try (CachedSupplier cachedSupplier = CachedSupplier.builder(supplier)
- .staleValueBehavior(ALLOW)
- .clock(clock)
- .jitterEnabled(false)
- .build()) {
- // Initial fetch
- supplier.set(RefreshResult.builder("value-1")
- .staleTime(now.plusSeconds(3600))
- .prefetchTime(now.plusSeconds(1800))
- .build());
- assertThat(cachedSupplier.get()).isEqualTo("value-1");
-
- // Invalidate with matching predicate
- clock.time = now.plusSeconds(10);
- cachedSupplier.invalidate(v -> v.equals("value-1"));
-
- // Set up a new value for the refresh
- supplier.set(RefreshResult.builder("value-2")
- .staleTime(now.plusSeconds(7200))
- .prefetchTime(now.plusSeconds(5400))
- .build());
-
- // Next get() should trigger refresh because staleTime was set to now
- assertThat(cachedSupplier.get()).isEqualTo("value-2");
- }
- }
-
- // --- Test 2: Predicate matching — false case ---
-
- @Test
- public void invalidate_predicateDoesNotMatch_nothingChanges() {
- AdjustableClock clock = new AdjustableClock();
- MutableSupplier supplier = new MutableSupplier();
- Instant now = Instant.parse("2024-01-01T00:00:00Z");
- clock.time = now;
-
- try (CachedSupplier cachedSupplier = CachedSupplier.builder(supplier)
- .staleValueBehavior(ALLOW)
- .clock(clock)
- .jitterEnabled(false)
- .build()) {
- // Initial fetch
- supplier.set(RefreshResult.builder("value-1")
- .staleTime(now.plusSeconds(3600))
- .prefetchTime(now.plusSeconds(1800))
- .build());
- assertThat(cachedSupplier.get()).isEqualTo("value-1");
-
- // Invalidate with non-matching predicate
- cachedSupplier.invalidate(v -> v.equals("different-value"));
-
- // Set a new value — should NOT be fetched since cache is still valid
- supplier.set(RefreshResult.builder("value-2")
- .staleTime(now.plusSeconds(7200))
- .prefetchTime(now.plusSeconds(5400))
- .build());
-
- // Should still return the original cached value
- assertThat(cachedSupplier.get()).isEqualTo("value-1");
- }
- }
-
- // --- Test 3: Null cached value ---
-
- @Test
- public void invalidate_nullCachedValue_doesNothing() {
- AdjustableClock clock = new AdjustableClock();
- MutableSupplier supplier = new MutableSupplier();
- Instant now = Instant.parse("2024-01-01T00:00:00Z");
- clock.time = now;
-
- try (CachedSupplier cachedSupplier = CachedSupplier.builder(supplier)
- .staleValueBehavior(ALLOW)
- .clock(clock)
- .jitterEnabled(false)
- .build()) {
- // Call invalidate before any get() — cachedValue is null
- // Should not throw
- cachedSupplier.invalidate(v -> true);
-
- // Now set up a supplier and get the value normally
- supplier.set(RefreshResult.builder("value-1")
- .staleTime(now.plusSeconds(3600))
- .prefetchTime(now.plusSeconds(1800))
- .build());
- assertThat(cachedSupplier.get()).isEqualTo("value-1");
- }
- }
-
- // --- Test 4: Value preservation — successful refresh returns fresh value ---
-
- @Test
- public void invalidate_successfulRefresh_returnsFreshValue() {
- AdjustableClock clock = new AdjustableClock();
- MutableSupplier supplier = new MutableSupplier();
- Instant now = Instant.parse("2024-01-01T00:00:00Z");
- clock.time = now;
-
- try (CachedSupplier cachedSupplier = CachedSupplier.builder(supplier)
- .staleValueBehavior(ALLOW)
- .clock(clock)
- .jitterEnabled(false)
- .build()) {
- // Initial fetch
- supplier.set(RefreshResult.builder("old-creds")
- .staleTime(now.plusSeconds(3600))
- .prefetchTime(now.plusSeconds(1800))
- .build());
- assertThat(cachedSupplier.get()).isEqualTo("old-creds");
-
- // Invalidate
- clock.time = now.plusSeconds(10);
- cachedSupplier.invalidate(v -> v.equals("old-creds"));
-
- // Supplier returns fresh value
- supplier.set(RefreshResult.builder("new-creds")
- .staleTime(now.plusSeconds(7200))
- .prefetchTime(now.plusSeconds(5400))
- .build());
-
- // get() triggers refresh and returns new value
- assertThat(cachedSupplier.get()).isEqualTo("new-creds");
- }
- }
-
- // --- Test 4b: Value preservation — backoff active returns stale cached value ---
-
- @Test
- public void invalidate_backoffActive_returnsStaleCachedValue() {
- AdjustableClock clock = new AdjustableClock();
- MutableSupplier supplier = new MutableSupplier();
- Instant now = Instant.parse("2024-01-01T00:00:00Z");
- clock.time = now;
-
- try (CachedSupplier cachedSupplier = CachedSupplier.builder(supplier)
- .staleValueBehavior(ALLOW)
- .clock(clock)
- .jitterEnabled(false)
- .build()) {
- // Initial fetch
- supplier.set(RefreshResult.builder("old-creds")
- .staleTime(now.plusSeconds(60))
- .prefetchTime(now.plusSeconds(30))
- .build());
- assertThat(cachedSupplier.get()).isEqualTo("old-creds");
-
- // Advance past stale time and trigger a failure to set nextAllowedRefreshTime
- clock.time = now.plusSeconds(61);
- supplier.set(new RuntimeException("service unavailable"));
- assertThat(cachedSupplier.get()).isEqualTo("old-creds");
-
- // Now nextAllowedRefreshTime is set (now+61 + [300,600]s)
- // Call invalidate — it should NOT clear nextAllowedRefreshTime
- clock.time = now.plusSeconds(62);
- cachedSupplier.invalidate(v -> v.equals("old-creds"));
-
- // Set up a fresh value in the supplier
- supplier.set(RefreshResult.builder("new-creds")
- .staleTime(Instant.MAX)
- .prefetchTime(Instant.MAX)
- .build());
-
- // get() should still return the cached (stale) value because backoff is active
- assertThat(cachedSupplier.get()).isEqualTo("old-creds");
- }
- }
-
- // --- Test 5: Backoff gating — invalidate does NOT clear nextAllowedRefreshTime ---
-
- @Test
- public void invalidate_doesNotClearNextAllowedRefreshTime() {
- AdjustableClock clock = new AdjustableClock();
- MutableSupplier supplier = new MutableSupplier();
- Instant now = Instant.parse("2024-01-01T00:00:00Z");
- clock.time = now;
-
- try (CachedSupplier cachedSupplier = CachedSupplier.builder(supplier)
- .staleValueBehavior(ALLOW)
- .clock(clock)
- .jitterEnabled(false)
- .build()) {
- // Initial fetch
- supplier.set(RefreshResult.builder("cached-creds")
- .staleTime(now.plusSeconds(60))
- .prefetchTime(now.plusSeconds(30))
- .build());
- assertThat(cachedSupplier.get()).isEqualTo("cached-creds");
-
- // Advance past stale time and trigger failure to set backoff gate
- clock.time = now.plusSeconds(61);
- supplier.set(new RuntimeException("service unavailable"));
- assertThat(cachedSupplier.get()).isEqualTo("cached-creds");
-
- // Backoff gate is now set to somewhere in [61+300, 61+600] seconds from 'now'
- // Call invalidate
- clock.time = now.plusSeconds(70);
- cachedSupplier.invalidate(v -> v.equals("cached-creds"));
-
- // Prepare a new value — but backoff should prevent refresh
- supplier.set(RefreshResult.builder("new-creds")
- .staleTime(Instant.MAX)
- .prefetchTime(Instant.MAX)
- .build());
-
- // Still within backoff — should return cached value
- clock.time = now.plusSeconds(100);
- assertThat(cachedSupplier.get()).isEqualTo("cached-creds");
-
- // Advance past maximum possible backoff (61 + 600 = 661s)
- clock.time = now.plusSeconds(700);
- // Now backoff has elapsed — refresh should succeed
- assertThat(cachedSupplier.get()).isEqualTo("new-creds");
- }
- }
-
- // --- Test 5b: After nextAllowedRefreshTime elapses, get() attempts refresh ---
-
- @Test
- public void afterBackoffElapses_getAttemptsRefresh() {
- AdjustableClock clock = new AdjustableClock();
- MutableSupplier supplier = new MutableSupplier();
- Instant now = Instant.parse("2024-01-01T00:00:00Z");
- clock.time = now;
-
- try (CachedSupplier cachedSupplier = CachedSupplier.builder(supplier)
- .staleValueBehavior(ALLOW)
- .clock(clock)
- .jitterEnabled(false)
- .build()) {
- // Initial fetch
- supplier.set(RefreshResult.builder("cached-creds")
- .staleTime(now.plusSeconds(60))
- .prefetchTime(now.plusSeconds(30))
- .build());
- assertThat(cachedSupplier.get()).isEqualTo("cached-creds");
-
- // Trigger failure and set backoff
- clock.time = now.plusSeconds(61);
- supplier.set(new RuntimeException("service unavailable"));
- assertThat(cachedSupplier.get()).isEqualTo("cached-creds");
-
- // Invalidate so staleTime = now
- clock.time = now.plusSeconds(62);
- cachedSupplier.invalidate(v -> v.equals("cached-creds"));
-
- // Prepare fresh value
- supplier.set(RefreshResult.builder("fresh-creds")
- .staleTime(now.plusSeconds(7200))
- .prefetchTime(now.plusSeconds(5400))
- .build());
-
- // Advance past maximum backoff: 61 + 600 = 661s from now
- clock.time = now.plusSeconds(700);
- // Backoff elapsed, staleTime (set to now by invalidate) is in the past → mandatory refresh
- assertThat(cachedSupplier.get()).isEqualTo("fresh-creds");
- }
- }
-
- // --- Test 6: Successful refresh clears nextAllowedRefreshTime ---
-
- @Test
- public void successfulRefresh_clearsNextAllowedRefreshTime() {
- AdjustableClock clock = new AdjustableClock();
- MutableSupplier supplier = new MutableSupplier();
- Instant now = Instant.parse("2024-01-01T00:00:00Z");
- clock.time = now;
-
- try (CachedSupplier cachedSupplier = CachedSupplier.builder(supplier)
- .staleValueBehavior(ALLOW)
- .clock(clock)
- .jitterEnabled(false)
- .build()) {
- // Initial fetch
- supplier.set(RefreshResult.builder("value-1")
- .staleTime(now.plusSeconds(60))
- .prefetchTime(now.plusSeconds(30))
- .build());
- assertThat(cachedSupplier.get()).isEqualTo("value-1");
-
- // Trigger failure — sets nextAllowedRefreshTime
- clock.time = now.plusSeconds(61);
- supplier.set(new RuntimeException("service unavailable"));
- assertThat(cachedSupplier.get()).isEqualTo("value-1");
-
- // Advance past maximum backoff and do a successful refresh
- clock.time = now.plusSeconds(700);
- supplier.set(RefreshResult.builder("value-2")
- .staleTime(now.plusSeconds(3700))
- .prefetchTime(now.plusSeconds(2000))
- .build());
- assertThat(cachedSupplier.get()).isEqualTo("value-2");
-
- // Now invalidate and verify immediate refresh happens (no backoff gate blocking)
- clock.time = now.plusSeconds(710);
- cachedSupplier.invalidate(v -> v.equals("value-2"));
-
- supplier.set(RefreshResult.builder("value-3")
- .staleTime(now.plusSeconds(7200))
- .prefetchTime(now.plusSeconds(5400))
- .build());
-
- // Since nextAllowedRefreshTime was cleared by the successful refresh,
- // the invalidation should trigger an immediate refresh
- assertThat(cachedSupplier.get()).isEqualTo("value-3");
- }
- }
-
- // --- Test 7: Thread safety ---
-
- @Test
- public void concurrentInvalidateAndGet_noCorruption() throws Exception {
- AdjustableClock clock = new AdjustableClock();
- Instant now = Instant.parse("2024-01-01T00:00:00Z");
- clock.time = now;
-
- AtomicInteger counter = new AtomicInteger(0);
- Supplier> supplier = () -> {
- String val = "value-" + counter.incrementAndGet();
- return RefreshResult.builder(val)
- .staleTime(now.plusSeconds(3600))
- .prefetchTime(now.plusSeconds(1800))
- .build();
- };
-
- try (CachedSupplier cachedSupplier = CachedSupplier.builder(supplier)
- .staleValueBehavior(ALLOW)
- .clock(clock)
- .jitterEnabled(false)
- .build()) {
- // Prime the cache
- assertThat(cachedSupplier.get()).isNotNull();
-
- int threadCount = 20;
- int iterations = 100;
- ExecutorService executor = Executors.newFixedThreadPool(threadCount);
- CountDownLatch startLatch = new CountDownLatch(1);
- List> futures = new ArrayList<>();
-
- for (int i = 0; i < threadCount; i++) {
- final int threadIdx = i;
- futures.add(executor.submit(() -> {
- try {
- startLatch.await();
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- return;
- }
- for (int j = 0; j < iterations; j++) {
- if (threadIdx % 2 == 0) {
- // Half the threads call invalidate
- cachedSupplier.invalidate(v -> true);
- } else {
- // Half the threads call get
- String value = cachedSupplier.get();
- assertThat(value).isNotNull();
- }
- }
- }));
- }
-
- // Release all threads simultaneously
- startLatch.countDown();
-
- // Wait for all to complete
- for (Future> future : futures) {
- future.get(30, TimeUnit.SECONDS);
- }
-
- executor.shutdown();
- assertThat(executor.awaitTermination(5, TimeUnit.SECONDS)).isTrue();
-
- // Final get() should return a non-null value
- assertThat(cachedSupplier.get()).isNotNull();
- }
- }
-
- // --- Test 8: Invalidate then refresh success (full cycle) ---
-
- @Test
- public void invalidateThenRefreshSuccess_fullCycle() {
- AdjustableClock clock = new AdjustableClock();
- MutableSupplier supplier = new MutableSupplier();
- Instant now = Instant.parse("2024-01-01T00:00:00Z");
- clock.time = now;
-
- try (CachedSupplier cachedSupplier = CachedSupplier.builder(supplier)
- .staleValueBehavior(ALLOW)
- .clock(clock)
- .jitterEnabled(false)
- .build()) {
- // Step 1: Initial get()
- supplier.set(RefreshResult.builder("initial-value")
- .staleTime(now.plusSeconds(3600))
- .prefetchTime(now.plusSeconds(1800))
- .build());
- assertThat(cachedSupplier.get()).isEqualTo("initial-value");
-
- // Step 2: invalidate()
- clock.time = now.plusSeconds(10);
- cachedSupplier.invalidate(v -> v.equals("initial-value"));
-
- // Step 3: get() triggers refresh → success returns new value
- supplier.set(RefreshResult.builder("refreshed-value")
- .staleTime(now.plusSeconds(7200))
- .prefetchTime(now.plusSeconds(5400))
- .build());
- assertThat(cachedSupplier.get()).isEqualTo("refreshed-value");
- }
- }
-
- // --- Test 9: Invalidate does NOT modify nextAllowedRefreshTime (explicit scenario) ---
-
- @Test
- public void invalidate_withActiveBackoff_doesNotModifyNextAllowedRefreshTime() {
- AdjustableClock clock = new AdjustableClock();
- MutableSupplier supplier = new MutableSupplier();
- Instant now = Instant.parse("2024-01-01T00:00:00Z");
- clock.time = now;
-
- try (CachedSupplier cachedSupplier = CachedSupplier.builder(supplier)
- .staleValueBehavior(ALLOW)
- .clock(clock)
- .jitterEnabled(false)
- .build()) {
- // Initial fetch
- supplier.set(RefreshResult.builder("cached-creds")
- .staleTime(now.plusSeconds(60))
- .prefetchTime(now.plusSeconds(30))
- .build());
- assertThat(cachedSupplier.get()).isEqualTo("cached-creds");
-
- // Advance past stale time and trigger failure to set nextAllowedRefreshTime
- clock.time = now.plusSeconds(61);
- supplier.set(new RuntimeException("temporary failure"));
- assertThat(cachedSupplier.get()).isEqualTo("cached-creds");
-
- // At this point, nextAllowedRefreshTime is set somewhere in [361, 661] seconds from 'now'
- // Call invalidate — should NOT modify nextAllowedRefreshTime
- clock.time = now.plusSeconds(65);
- cachedSupplier.invalidate(v -> v.equals("cached-creds"));
-
- // Prepare a new value that would be returned if refresh is attempted
- supplier.set(RefreshResult.builder("new-creds")
- .staleTime(Instant.MAX)
- .prefetchTime(Instant.MAX)
- .build());
-
- // Still within the backoff window (65s < minimum backoff end at 361s)
- // get() should return cached value without attempting refresh
- assertThat(cachedSupplier.get()).isEqualTo("cached-creds");
-
- // Advance slightly — still within backoff range
- clock.time = now.plusSeconds(200);
- assertThat(cachedSupplier.get()).isEqualTo("cached-creds");
-
- // Advance past maximum possible backoff (61 + 600 = 661s)
- clock.time = now.plusSeconds(700);
- // Now the backoff has elapsed — refresh should be attempted and succeed
- assertThat(cachedSupplier.get()).isEqualTo("new-creds");
- }
- }
-
- // --- Helper classes ---
-
- private static class MutableSupplier implements Supplier> {
- private volatile RuntimeException thingToThrow;
- private volatile RefreshResult thingToReturn;
-
- @Override
- public RefreshResult get() {
- if (thingToThrow != null) {
- throw thingToThrow;
- }
- return thingToReturn;
- }
-
- private MutableSupplier set(RuntimeException exception) {
- this.thingToThrow = exception;
- this.thingToReturn = null;
- return this;
- }
-
- private MutableSupplier set(RefreshResult value) {
- this.thingToThrow = null;
- this.thingToReturn = value;
- return this;
- }
- }
-
- private static class AdjustableClock extends Clock {
- private volatile Instant time;
-
- @Override
- public ZoneId getZone() {
- return ZoneOffset.UTC;
- }
-
- @Override
- public Clock withZone(ZoneId zone) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public Instant instant() {
- return time;
- }
- }
-}
diff --git a/utils/src/test/java/software/amazon/awssdk/utils/cache/CachedSupplierTest.java b/utils/src/test/java/software/amazon/awssdk/utils/cache/CachedSupplierTest.java
index fc6318104f4b..e6bb3bbf1bdc 100644
--- a/utils/src/test/java/software/amazon/awssdk/utils/cache/CachedSupplierTest.java
+++ b/utils/src/test/java/software/amazon/awssdk/utils/cache/CachedSupplierTest.java
@@ -35,11 +35,13 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -868,4 +870,147 @@ public Instant instant() {
return time;
}
}
+
+ // --- invalidate() tests ---
+
+ @Test
+ public void invalidate_predicateMatches_triggersRefresh() {
+ AdjustableClock clock = new AdjustableClock();
+ MutableSupplier supplier = new MutableSupplier();
+ Instant now = Instant.parse("2024-01-01T00:00:00Z");
+ clock.time = now;
+
+ try (CachedSupplier cache = CachedSupplier.builder(supplier)
+ .staleValueBehavior(ALLOW)
+ .clock(clock)
+ .jitterEnabled(false)
+ .build()) {
+ supplier.set(RefreshResult.builder("value-1").staleTime(now.plusSeconds(3600)).prefetchTime(now.plusSeconds(1800)).build());
+ assertThat(cache.get()).isEqualTo("value-1");
+
+ clock.time = now.plusSeconds(10);
+ cache.invalidate(v -> v.equals("value-1"));
+
+ supplier.set(RefreshResult.builder("value-2").staleTime(now.plusSeconds(7200)).prefetchTime(now.plusSeconds(5400)).build());
+ assertThat(cache.get()).isEqualTo("value-2");
+ }
+ }
+
+ @Test
+ public void invalidate_predicateDoesNotMatch_doesNotTriggerRefresh() {
+ AdjustableClock clock = new AdjustableClock();
+ MutableSupplier supplier = new MutableSupplier();
+ Instant now = Instant.parse("2024-01-01T00:00:00Z");
+ clock.time = now;
+
+ try (CachedSupplier cache = CachedSupplier.builder(supplier)
+ .staleValueBehavior(ALLOW)
+ .clock(clock)
+ .jitterEnabled(false)
+ .build()) {
+ supplier.set(RefreshResult.builder("value-1").staleTime(now.plusSeconds(3600)).prefetchTime(now.plusSeconds(1800)).build());
+ assertThat(cache.get()).isEqualTo("value-1");
+
+ cache.invalidate(v -> v.equals("different-value"));
+
+ supplier.set(RefreshResult.builder("value-2").staleTime(now.plusSeconds(7200)).prefetchTime(now.plusSeconds(5400)).build());
+ assertThat(cache.get()).isEqualTo("value-1");
+ }
+ }
+
+ @Test
+ public void invalidate_beforeFirstGet_isNoOp() {
+ AdjustableClock clock = new AdjustableClock();
+ MutableSupplier supplier = new MutableSupplier();
+ clock.time = Instant.parse("2024-01-01T00:00:00Z");
+
+ try (CachedSupplier cache = CachedSupplier.builder(supplier)
+ .staleValueBehavior(ALLOW)
+ .clock(clock)
+ .jitterEnabled(false)
+ .build()) {
+ cache.invalidate(v -> true); // should not throw
+
+ supplier.set(RefreshResult.builder("value-1").staleTime(Instant.MAX).prefetchTime(Instant.MAX).build());
+ assertThat(cache.get()).isEqualTo("value-1");
+ }
+ }
+
+ @Test
+ public void invalidate_doesNotBypassRefreshBackoff() {
+ AdjustableClock clock = new AdjustableClock();
+ MutableSupplier supplier = new MutableSupplier();
+ Instant now = Instant.parse("2024-01-01T00:00:00Z");
+ clock.time = now;
+
+ try (CachedSupplier cache = CachedSupplier.builder(supplier)
+ .staleValueBehavior(ALLOW)
+ .clock(clock)
+ .jitterEnabled(false)
+ .build()) {
+ supplier.set(RefreshResult.builder("old").staleTime(now.plusSeconds(60)).prefetchTime(now.plusSeconds(30)).build());
+ assertThat(cache.get()).isEqualTo("old");
+
+ // Trigger failure to set backoff
+ clock.time = now.plusSeconds(61);
+ supplier.set(new RuntimeException("unavailable"));
+ assertThat(cache.get()).isEqualTo("old");
+
+ // Invalidate — marks stale but doesn't clear backoff
+ clock.time = now.plusSeconds(62);
+ cache.invalidate(v -> v.equals("old"));
+ supplier.set(RefreshResult.builder("new").staleTime(Instant.MAX).prefetchTime(Instant.MAX).build());
+
+ // Still within backoff — returns stale
+ assertThat(cache.get()).isEqualTo("old");
+
+ // Past backoff — returns fresh
+ clock.time = now.plusSeconds(700);
+ assertThat(cache.get()).isEqualTo("new");
+ }
+ }
+
+ @Test
+ public void invalidate_concurrentWithGet_doesNotCorrupt() throws Exception {
+ AdjustableClock clock = new AdjustableClock();
+ clock.time = Instant.parse("2024-01-01T00:00:00Z");
+ AtomicInteger counter = new AtomicInteger(0);
+
+ try (CachedSupplier cache = CachedSupplier.builder(() ->
+ RefreshResult.builder("v-" + counter.incrementAndGet())
+ .staleTime(Instant.MAX)
+ .prefetchTime(Instant.MAX)
+ .build())
+ .staleValueBehavior(ALLOW)
+ .clock(clock)
+ .jitterEnabled(false)
+ .build()) {
+
+ cache.get(); // prime
+
+ ExecutorService executor = Executors.newFixedThreadPool(10);
+ CountDownLatch start = new CountDownLatch(1);
+ List> futures = new ArrayList<>();
+
+ for (int i = 0; i < 10; i++) {
+ int idx = i;
+ futures.add(executor.submit(() -> {
+ try { start.await(); } catch (InterruptedException e) { return; }
+ for (int j = 0; j < 50; j++) {
+ if (idx % 2 == 0) {
+ cache.invalidate(v -> true);
+ } else {
+ assertThat(cache.get()).isNotNull();
+ }
+ }
+ }));
+ }
+
+ start.countDown();
+ for (Future> f : futures) { f.get(30, TimeUnit.SECONDS); }
+ executor.shutdown();
+
+ assertThat(cache.get()).isNotNull();
+ }
+ }
}
From 499a0950f225c236ce569ed35356f429e8e716a6 Mon Sep 17 00:00:00 2001
From: Alex Woods
Date: Thu, 9 Jul 2026 13:28:00 -0700
Subject: [PATCH 5/9] Switch to builder for SelectedAuthScheme instead of 4 arg
constructor
---
.../amazon/awssdk/spotbugs-suppressions.xml | 1 +
.../codegen/poet/client/ClientClassUtils.java | 6 +-
.../poet/rules/EndpointResolverUtilsSpec.java | 15 ++--
...gned-payload-trait-async-client-class.java | 3 +-
...igned-payload-trait-sync-client-class.java | 3 +-
.../endpoint-resolve-interceptor-preSra.java | 4 +-
...e-interceptor-with-endpointsbasedauth.java | 7 +-
...olve-interceptor-with-multiauthsigv4a.java | 7 +-
...-resolve-interceptor-with-stringarray.java | 4 +-
.../rules/endpoint-resolve-interceptor.java | 4 +-
...esolver-utils-with-endpointsbasedauth.java | 4 +-
...t-resolver-utils-with-multiauthsigv4a.java | 4 +-
...point-resolver-utils-with-stringarray.java | 4 +-
.../poet/rules/endpoint-resolver-utils.java | 4 +-
.../awssdk/core/SelectedAuthScheme.java | 85 +++++++++++++++++--
.../core/http/auth/AuthSchemeResolver.java | 41 +++++----
.../stages/AuthSchemeResolutionStage.java | 7 +-
.../AuthErrorInvalidationHelperTest.java | 24 +++---
18 files changed, 155 insertions(+), 72 deletions(-)
diff --git a/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml b/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml
index 05606dc6d574..36cfc8b66070 100644
--- a/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml
+++ b/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml
@@ -345,6 +345,7 @@
+
diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/ClientClassUtils.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/ClientClassUtils.java
index 9684be34b02e..67401329cc01 100644
--- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/ClientClassUtils.java
+++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/ClientClassUtils.java
@@ -564,8 +564,10 @@ static MethodSpec resolveEndpointMethod(AuthSchemeSpecUtils authSchemeSpecUtils,
authSchemeOption.nestedClass("Builder"));
b.addStatement("$1T rs = $1T.create(endpointParams.region().id())", regionSet);
b.addStatement("optionBuilder.putSignerProperty($T.REGION_SET, rs)", awsV4aHttpSigner);
- b.addStatement("selectedAuthScheme = new $T(selectedAuthScheme.identity(), selectedAuthScheme.signer(), "
- + "optionBuilder.build())", SelectedAuthScheme.class);
+ b.addStatement("selectedAuthScheme = $T.builder().identity(selectedAuthScheme.identity())"
+ + ".signer(selectedAuthScheme.signer()).authSchemeOption(optionBuilder.build())"
+ + ".identityProvider(selectedAuthScheme.identityProvider()).build()",
+ SelectedAuthScheme.class);
b.endControlFlow();
}
diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverUtilsSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverUtilsSpec.java
index 09eb138689b1..013a6a54d602 100644
--- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverUtilsSpec.java
+++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverUtilsSpec.java
@@ -602,8 +602,9 @@ private static CodeBlock copyV4EndpointSignerPropertiesToAuth() {
code.addStatement("option.putSignerProperty($T.SERVICE_SIGNING_NAME, v4AuthScheme.signingName())",
AwsV4HttpSigner.class);
code.endControlFlow();
- code.addStatement("return new $T<>(selectedAuthScheme.identity(), selectedAuthScheme.signer(), option.build(), "
- + "selectedAuthScheme.identityProvider())",
+ code.addStatement("return $T.builder().identity(selectedAuthScheme.identity())"
+ + ".signer(selectedAuthScheme.signer()).authSchemeOption(option.build())"
+ + ".identityProvider(selectedAuthScheme.identityProvider()).build()",
SelectedAuthScheme.class);
code.endControlFlow();
return code.build();
@@ -632,8 +633,9 @@ private CodeBlock copyV4aEndpointSignerPropertiesToAuth() {
code.addStatement("option.putSignerProperty($T.SERVICE_SIGNING_NAME, v4aAuthScheme.signingName())",
AwsV4aHttpSigner.class);
code.endControlFlow();
- code.addStatement("return new $T<>(selectedAuthScheme.identity(), selectedAuthScheme.signer(), option.build(), "
- + "selectedAuthScheme.identityProvider())",
+ code.addStatement("return $T.builder().identity(selectedAuthScheme.identity())"
+ + ".signer(selectedAuthScheme.signer()).authSchemeOption(option.build())"
+ + ".identityProvider(selectedAuthScheme.identityProvider()).build()",
SelectedAuthScheme.class);
code.endControlFlow();
return code.build();
@@ -658,8 +660,9 @@ private CodeBlock copyS3ExpressEndpointSignerPropertiesToAuth() {
code.addStatement("option.putSignerProperty($T.SERVICE_SIGNING_NAME, s3ExpressAuthScheme.signingName())",
AwsV4HttpSigner.class);
code.endControlFlow();
- code.addStatement("return new $T<>(selectedAuthScheme.identity(), selectedAuthScheme.signer(), option.build(), "
- + "selectedAuthScheme.identityProvider())",
+ code.addStatement("return $T.builder().identity(selectedAuthScheme.identity())"
+ + ".signer(selectedAuthScheme.signer()).authSchemeOption(option.build())"
+ + ".identityProvider(selectedAuthScheme.identityProvider()).build()",
SelectedAuthScheme.class);
code.endControlFlow();
return code.build();
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-unsigned-payload-trait-async-client-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-unsigned-payload-trait-async-client-class.java
index 3e76e9b15269..0283255249f7 100644
--- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-unsigned-payload-trait-async-client-class.java
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-unsigned-payload-trait-async-client-class.java
@@ -1080,8 +1080,7 @@ private Endpoint resolveEndpoint(SdkRequest request, ExecutionAttributes executi
AuthSchemeOption.Builder optionBuilder = selectedAuthScheme.authSchemeOption().toBuilder();
RegionSet rs = RegionSet.create(endpointParams.region().id());
optionBuilder.putSignerProperty(AwsV4aHttpSigner.REGION_SET, rs);
- selectedAuthScheme = new SelectedAuthScheme(selectedAuthScheme.identity(), selectedAuthScheme.signer(),
- optionBuilder.build());
+ selectedAuthScheme = SelectedAuthScheme.builder().identity(selectedAuthScheme.identity()).signer(selectedAuthScheme.signer()).authSchemeOption(optionBuilder.build()).identityProvider(selectedAuthScheme.identityProvider()).build();
}
executionAttributes.putAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME, selectedAuthScheme);
}
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-unsigned-payload-trait-sync-client-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-unsigned-payload-trait-sync-client-class.java
index 4d7ce7f0f45c..57be531f36ea 100644
--- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-unsigned-payload-trait-sync-client-class.java
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-unsigned-payload-trait-sync-client-class.java
@@ -965,8 +965,7 @@ private Endpoint resolveEndpoint(SdkRequest request, ExecutionAttributes executi
AuthSchemeOption.Builder optionBuilder = selectedAuthScheme.authSchemeOption().toBuilder();
RegionSet rs = RegionSet.create(endpointParams.region().id());
optionBuilder.putSignerProperty(AwsV4aHttpSigner.REGION_SET, rs);
- selectedAuthScheme = new SelectedAuthScheme(selectedAuthScheme.identity(), selectedAuthScheme.signer(),
- optionBuilder.build());
+ selectedAuthScheme = SelectedAuthScheme.builder().identity(selectedAuthScheme.identity()).signer(selectedAuthScheme.signer()).authSchemeOption(optionBuilder.build()).identityProvider(selectedAuthScheme.identityProvider()).build();
}
executionAttributes.putAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME, selectedAuthScheme);
}
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-preSra.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-preSra.java
index cf27b03ed0c3..792febf9c653 100644
--- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-preSra.java
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-preSra.java
@@ -177,7 +177,7 @@ private SelectedAuthScheme authSchemeWithEndpointSignerP
if (v4AuthScheme.signingName() != null) {
option.putSignerProperty(AwsV4HttpSigner.SERVICE_SIGNING_NAME, v4AuthScheme.signingName());
}
- return new SelectedAuthScheme<>(selectedAuthScheme.identity(), selectedAuthScheme.signer(), option.build());
+ return SelectedAuthScheme.builder().identity(selectedAuthScheme.identity()).signer(selectedAuthScheme.signer()).authSchemeOption(option.build()).identityProvider(selectedAuthScheme.identityProvider()).build();
}
if (endpointAuthScheme instanceof SigV4aAuthScheme) {
SigV4aAuthScheme v4aAuthScheme = (SigV4aAuthScheme) endpointAuthScheme;
@@ -191,7 +191,7 @@ private SelectedAuthScheme authSchemeWithEndpointSignerP
if (v4aAuthScheme.signingName() != null) {
option.putSignerProperty(AwsV4aHttpSigner.SERVICE_SIGNING_NAME, v4aAuthScheme.signingName());
}
- return new SelectedAuthScheme<>(selectedAuthScheme.identity(), selectedAuthScheme.signer(), option.build());
+ return SelectedAuthScheme.builder().identity(selectedAuthScheme.identity()).signer(selectedAuthScheme.signer()).authSchemeOption(option.build()).identityProvider(selectedAuthScheme.identityProvider()).build();
}
throw new IllegalArgumentException("Endpoint auth scheme '" + endpointAuthScheme.name()
+ "' cannot be mapped to the SDK auth scheme. Was it declared in the service's model?");
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-endpointsbasedauth.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-endpointsbasedauth.java
index 6d6aa1dc2138..9e95995a17de 100644
--- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-endpointsbasedauth.java
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-endpointsbasedauth.java
@@ -81,8 +81,7 @@ public SdkRequest modifyRequest(Context.ModifyRequest context, ExecutionAttribut
AuthSchemeOption.Builder optionBuilder = selectedAuthScheme.authSchemeOption().toBuilder();
RegionSet regionSet = RegionSet.create(endpointParams.region().id());
optionBuilder.putSignerProperty(AwsV4aHttpSigner.REGION_SET, regionSet);
- selectedAuthScheme = new SelectedAuthScheme(selectedAuthScheme.identity(), selectedAuthScheme.signer(),
- optionBuilder.build());
+ selectedAuthScheme = SelectedAuthScheme.builder().identity(selectedAuthScheme.identity()).signer(selectedAuthScheme.signer()).authSchemeOption(optionBuilder.build()).identityProvider(selectedAuthScheme.identityProvider()).build();
}
executionAttributes.putAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME, selectedAuthScheme);
}
@@ -172,7 +171,7 @@ private SelectedAuthScheme authSchemeWithEndpointSignerP
if (v4AuthScheme.signingName() != null) {
option.putSignerProperty(AwsV4HttpSigner.SERVICE_SIGNING_NAME, v4AuthScheme.signingName());
}
- return new SelectedAuthScheme<>(selectedAuthScheme.identity(), selectedAuthScheme.signer(), option.build());
+ return SelectedAuthScheme.builder().identity(selectedAuthScheme.identity()).signer(selectedAuthScheme.signer()).authSchemeOption(option.build()).identityProvider(selectedAuthScheme.identityProvider()).build();
}
if (endpointAuthScheme instanceof SigV4aAuthScheme) {
SigV4aAuthScheme v4aAuthScheme = (SigV4aAuthScheme) endpointAuthScheme;
@@ -188,7 +187,7 @@ private SelectedAuthScheme authSchemeWithEndpointSignerP
if (v4aAuthScheme.signingName() != null) {
option.putSignerProperty(AwsV4aHttpSigner.SERVICE_SIGNING_NAME, v4aAuthScheme.signingName());
}
- return new SelectedAuthScheme<>(selectedAuthScheme.identity(), selectedAuthScheme.signer(), option.build());
+ return SelectedAuthScheme.builder().identity(selectedAuthScheme.identity()).signer(selectedAuthScheme.signer()).authSchemeOption(option.build()).identityProvider(selectedAuthScheme.identityProvider()).build();
}
throw new IllegalArgumentException("Endpoint auth scheme '" + endpointAuthScheme.name()
+ "' cannot be mapped to the SDK auth scheme. Was it declared in the service's model?");
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-multiauthsigv4a.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-multiauthsigv4a.java
index 858d39fc7a69..884e9ca4eda9 100644
--- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-multiauthsigv4a.java
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-multiauthsigv4a.java
@@ -70,8 +70,7 @@ public SdkRequest modifyRequest(Context.ModifyRequest context, ExecutionAttribut
AuthSchemeOption.Builder optionBuilder = selectedAuthScheme.authSchemeOption().toBuilder();
RegionSet regionSet = RegionSet.create(endpointParams.region().id());
optionBuilder.putSignerProperty(AwsV4aHttpSigner.REGION_SET, regionSet);
- selectedAuthScheme = new SelectedAuthScheme(selectedAuthScheme.identity(), selectedAuthScheme.signer(),
- optionBuilder.build());
+ selectedAuthScheme = SelectedAuthScheme.builder().identity(selectedAuthScheme.identity()).signer(selectedAuthScheme.signer()).authSchemeOption(optionBuilder.build()).identityProvider(selectedAuthScheme.identityProvider()).build();
}
executionAttributes.putAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME, selectedAuthScheme);
}
@@ -135,7 +134,7 @@ private SelectedAuthScheme authSchemeWithEndpointSignerP
if (v4AuthScheme.signingName() != null) {
option.putSignerProperty(AwsV4HttpSigner.SERVICE_SIGNING_NAME, v4AuthScheme.signingName());
}
- return new SelectedAuthScheme<>(selectedAuthScheme.identity(), selectedAuthScheme.signer(), option.build());
+ return SelectedAuthScheme.builder().identity(selectedAuthScheme.identity()).signer(selectedAuthScheme.signer()).authSchemeOption(option.build()).identityProvider(selectedAuthScheme.identityProvider()).build();
}
if (endpointAuthScheme instanceof SigV4aAuthScheme) {
SigV4aAuthScheme v4aAuthScheme = (SigV4aAuthScheme) endpointAuthScheme;
@@ -151,7 +150,7 @@ private SelectedAuthScheme authSchemeWithEndpointSignerP
if (v4aAuthScheme.signingName() != null) {
option.putSignerProperty(AwsV4aHttpSigner.SERVICE_SIGNING_NAME, v4aAuthScheme.signingName());
}
- return new SelectedAuthScheme<>(selectedAuthScheme.identity(), selectedAuthScheme.signer(), option.build());
+ return SelectedAuthScheme.builder().identity(selectedAuthScheme.identity()).signer(selectedAuthScheme.signer()).authSchemeOption(option.build()).identityProvider(selectedAuthScheme.identityProvider()).build();
}
throw new IllegalArgumentException("Endpoint auth scheme '" + endpointAuthScheme.name()
+ "' cannot be mapped to the SDK auth scheme. Was it declared in the service's model?");
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-stringarray.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-stringarray.java
index 0f5033376ffc..b86e695868ce 100644
--- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-stringarray.java
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-stringarray.java
@@ -144,7 +144,7 @@ private SelectedAuthScheme authSchemeWithEndpointSignerP
if (v4AuthScheme.signingName() != null) {
option.putSignerProperty(AwsV4HttpSigner.SERVICE_SIGNING_NAME, v4AuthScheme.signingName());
}
- return new SelectedAuthScheme<>(selectedAuthScheme.identity(), selectedAuthScheme.signer(), option.build());
+ return SelectedAuthScheme.builder().identity(selectedAuthScheme.identity()).signer(selectedAuthScheme.signer()).authSchemeOption(option.build()).identityProvider(selectedAuthScheme.identityProvider()).build();
}
if (endpointAuthScheme instanceof SigV4aAuthScheme) {
SigV4aAuthScheme v4aAuthScheme = (SigV4aAuthScheme) endpointAuthScheme;
@@ -158,7 +158,7 @@ private SelectedAuthScheme authSchemeWithEndpointSignerP
if (v4aAuthScheme.signingName() != null) {
option.putSignerProperty(AwsV4aHttpSigner.SERVICE_SIGNING_NAME, v4aAuthScheme.signingName());
}
- return new SelectedAuthScheme<>(selectedAuthScheme.identity(), selectedAuthScheme.signer(), option.build());
+ return SelectedAuthScheme.builder().identity(selectedAuthScheme.identity()).signer(selectedAuthScheme.signer()).authSchemeOption(option.build()).identityProvider(selectedAuthScheme.identityProvider()).build();
}
throw new IllegalArgumentException("Endpoint auth scheme '" + endpointAuthScheme.name()
+ "' cannot be mapped to the SDK auth scheme. Was it declared in the service's model?");
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor.java
index abdfedab0455..59e6af3b36f5 100644
--- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor.java
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor.java
@@ -163,7 +163,7 @@ private SelectedAuthScheme authSchemeWithEndpointSignerP
if (v4AuthScheme.signingName() != null) {
option.putSignerProperty(AwsV4HttpSigner.SERVICE_SIGNING_NAME, v4AuthScheme.signingName());
}
- return new SelectedAuthScheme<>(selectedAuthScheme.identity(), selectedAuthScheme.signer(), option.build());
+ return SelectedAuthScheme.builder().identity(selectedAuthScheme.identity()).signer(selectedAuthScheme.signer()).authSchemeOption(option.build()).identityProvider(selectedAuthScheme.identityProvider()).build();
}
if (endpointAuthScheme instanceof SigV4aAuthScheme) {
SigV4aAuthScheme v4aAuthScheme = (SigV4aAuthScheme) endpointAuthScheme;
@@ -177,7 +177,7 @@ private SelectedAuthScheme authSchemeWithEndpointSignerP
if (v4aAuthScheme.signingName() != null) {
option.putSignerProperty(AwsV4aHttpSigner.SERVICE_SIGNING_NAME, v4aAuthScheme.signingName());
}
- return new SelectedAuthScheme<>(selectedAuthScheme.identity(), selectedAuthScheme.signer(), option.build());
+ return SelectedAuthScheme.builder().identity(selectedAuthScheme.identity()).signer(selectedAuthScheme.signer()).authSchemeOption(option.build()).identityProvider(selectedAuthScheme.identityProvider()).build();
}
throw new IllegalArgumentException("Endpoint auth scheme '" + endpointAuthScheme.name()
+ "' cannot be mapped to the SDK auth scheme. Was it declared in the service's model?");
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils-with-endpointsbasedauth.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils-with-endpointsbasedauth.java
index ad3b2e674750..5197385b7c28 100644
--- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils-with-endpointsbasedauth.java
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils-with-endpointsbasedauth.java
@@ -101,7 +101,7 @@ public static SelectedAuthScheme authSchemeWithEndpointS
if (v4AuthScheme.signingName() != null) {
option.putSignerProperty(AwsV4HttpSigner.SERVICE_SIGNING_NAME, v4AuthScheme.signingName());
}
- return new SelectedAuthScheme<>(selectedAuthScheme.identity(), selectedAuthScheme.signer(), option.build());
+ return SelectedAuthScheme.builder().identity(selectedAuthScheme.identity()).signer(selectedAuthScheme.signer()).authSchemeOption(option.build()).identityProvider(selectedAuthScheme.identityProvider()).build();
}
if (endpointAuthScheme instanceof SigV4aAuthScheme) {
SigV4aAuthScheme v4aAuthScheme = (SigV4aAuthScheme) endpointAuthScheme;
@@ -117,7 +117,7 @@ public static SelectedAuthScheme authSchemeWithEndpointS
if (v4aAuthScheme.signingName() != null) {
option.putSignerProperty(AwsV4aHttpSigner.SERVICE_SIGNING_NAME, v4aAuthScheme.signingName());
}
- return new SelectedAuthScheme<>(selectedAuthScheme.identity(), selectedAuthScheme.signer(), option.build());
+ return SelectedAuthScheme.builder().identity(selectedAuthScheme.identity()).signer(selectedAuthScheme.signer()).authSchemeOption(option.build()).identityProvider(selectedAuthScheme.identityProvider()).build();
}
throw new IllegalArgumentException("Endpoint auth scheme '" + endpointAuthScheme.name()
+ "' cannot be mapped to the SDK auth scheme. Was it declared in the service's model?");
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils-with-multiauthsigv4a.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils-with-multiauthsigv4a.java
index f4a83932a826..1030af0199ca 100644
--- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils-with-multiauthsigv4a.java
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils-with-multiauthsigv4a.java
@@ -64,7 +64,7 @@ public static SelectedAuthScheme authSchemeWithEndpointS
if (v4AuthScheme.signingName() != null) {
option.putSignerProperty(AwsV4HttpSigner.SERVICE_SIGNING_NAME, v4AuthScheme.signingName());
}
- return new SelectedAuthScheme<>(selectedAuthScheme.identity(), selectedAuthScheme.signer(), option.build());
+ return SelectedAuthScheme.builder().identity(selectedAuthScheme.identity()).signer(selectedAuthScheme.signer()).authSchemeOption(option.build()).identityProvider(selectedAuthScheme.identityProvider()).build();
}
if (endpointAuthScheme instanceof SigV4aAuthScheme) {
SigV4aAuthScheme v4aAuthScheme = (SigV4aAuthScheme) endpointAuthScheme;
@@ -80,7 +80,7 @@ public static SelectedAuthScheme authSchemeWithEndpointS
if (v4aAuthScheme.signingName() != null) {
option.putSignerProperty(AwsV4aHttpSigner.SERVICE_SIGNING_NAME, v4aAuthScheme.signingName());
}
- return new SelectedAuthScheme<>(selectedAuthScheme.identity(), selectedAuthScheme.signer(), option.build());
+ return SelectedAuthScheme.builder().identity(selectedAuthScheme.identity()).signer(selectedAuthScheme.signer()).authSchemeOption(option.build()).identityProvider(selectedAuthScheme.identityProvider()).build();
}
throw new IllegalArgumentException("Endpoint auth scheme '" + endpointAuthScheme.name()
+ "' cannot be mapped to the SDK auth scheme. Was it declared in the service's model?");
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils-with-stringarray.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils-with-stringarray.java
index aaed43624739..e85d33c4b7dc 100644
--- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils-with-stringarray.java
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils-with-stringarray.java
@@ -83,7 +83,7 @@ public static SelectedAuthScheme authSchemeWithEndpointS
if (v4AuthScheme.signingName() != null) {
option.putSignerProperty(AwsV4HttpSigner.SERVICE_SIGNING_NAME, v4AuthScheme.signingName());
}
- return new SelectedAuthScheme<>(selectedAuthScheme.identity(), selectedAuthScheme.signer(), option.build());
+ return SelectedAuthScheme.builder().identity(selectedAuthScheme.identity()).signer(selectedAuthScheme.signer()).authSchemeOption(option.build()).identityProvider(selectedAuthScheme.identityProvider()).build();
}
if (endpointAuthScheme instanceof SigV4aAuthScheme) {
SigV4aAuthScheme v4aAuthScheme = (SigV4aAuthScheme) endpointAuthScheme;
@@ -97,7 +97,7 @@ public static SelectedAuthScheme authSchemeWithEndpointS
if (v4aAuthScheme.signingName() != null) {
option.putSignerProperty(AwsV4aHttpSigner.SERVICE_SIGNING_NAME, v4aAuthScheme.signingName());
}
- return new SelectedAuthScheme<>(selectedAuthScheme.identity(), selectedAuthScheme.signer(), option.build());
+ return SelectedAuthScheme.builder().identity(selectedAuthScheme.identity()).signer(selectedAuthScheme.signer()).authSchemeOption(option.build()).identityProvider(selectedAuthScheme.identityProvider()).build();
}
throw new IllegalArgumentException("Endpoint auth scheme '" + endpointAuthScheme.name() + "' cannot be mapped to the SDK auth scheme. Was it declared in the service's model?");
}
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils.java
index 01c7ba43831f..f213bf77f076 100644
--- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils.java
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils.java
@@ -101,7 +101,7 @@ public static SelectedAuthScheme authSchemeWithEndpointS
if (v4AuthScheme.signingName() != null) {
option.putSignerProperty(AwsV4HttpSigner.SERVICE_SIGNING_NAME, v4AuthScheme.signingName());
}
- return new SelectedAuthScheme<>(selectedAuthScheme.identity(), selectedAuthScheme.signer(), option.build());
+ return SelectedAuthScheme.builder().identity(selectedAuthScheme.identity()).signer(selectedAuthScheme.signer()).authSchemeOption(option.build()).identityProvider(selectedAuthScheme.identityProvider()).build();
}
if (endpointAuthScheme instanceof SigV4aAuthScheme) {
SigV4aAuthScheme v4aAuthScheme = (SigV4aAuthScheme) endpointAuthScheme;
@@ -115,7 +115,7 @@ public static SelectedAuthScheme authSchemeWithEndpointS
if (v4aAuthScheme.signingName() != null) {
option.putSignerProperty(AwsV4aHttpSigner.SERVICE_SIGNING_NAME, v4aAuthScheme.signingName());
}
- return new SelectedAuthScheme<>(selectedAuthScheme.identity(), selectedAuthScheme.signer(), option.build());
+ return SelectedAuthScheme.builder().identity(selectedAuthScheme.identity()).signer(selectedAuthScheme.signer()).authSchemeOption(option.build()).identityProvider(selectedAuthScheme.identityProvider()).build();
}
throw new IllegalArgumentException("Endpoint auth scheme '" + endpointAuthScheme.name()
+ "' cannot be mapped to the SDK auth scheme. Was it declared in the service's model?");
diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/SelectedAuthScheme.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/SelectedAuthScheme.java
index 4542bb71054a..232aae6aca25 100644
--- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/SelectedAuthScheme.java
+++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/SelectedAuthScheme.java
@@ -43,6 +43,7 @@
/// - identity: CompletableFuture ← the resolved identity!
/// - signer: HttpSigner
/// - authSchemeOption: AuthSchemeOption
+/// - identityProvider: IdentityProvider ← the provider that resolved the identity
/// ```
@SdkProtectedApi
public final class SelectedAuthScheme {
@@ -51,20 +52,24 @@ public final class SelectedAuthScheme {
private final AuthSchemeOption authSchemeOption;
private final IdentityProvider identityProvider;
+ /**
+ * @deprecated Use {@link #builder()} instead.
+ */
+ @Deprecated
public SelectedAuthScheme(CompletableFuture extends T> identity,
HttpSigner signer,
AuthSchemeOption authSchemeOption) {
- this(identity, signer, authSchemeOption, null);
- }
-
- public SelectedAuthScheme(CompletableFuture extends T> identity,
- HttpSigner signer,
- AuthSchemeOption authSchemeOption,
- IdentityProvider identityProvider) {
this.identity = Validate.paramNotNull(identity, "identity");
this.signer = Validate.paramNotNull(signer, "signer");
this.authSchemeOption = Validate.paramNotNull(authSchemeOption, "authSchemeOption");
- this.identityProvider = identityProvider; // nullable for backward compat
+ this.identityProvider = null;
+ }
+
+ private SelectedAuthScheme(BuilderImpl builder) {
+ this.identity = Validate.paramNotNull(builder.identity, "identity");
+ this.signer = Validate.paramNotNull(builder.signer, "signer");
+ this.authSchemeOption = Validate.paramNotNull(builder.authSchemeOption, "authSchemeOption");
+ this.identityProvider = builder.identityProvider;
}
public CompletableFuture extends T> identity() {
@@ -82,4 +87,68 @@ public AuthSchemeOption authSchemeOption() {
public IdentityProvider identityProvider() {
return identityProvider;
}
+
+ public static Builder builder() {
+ return new BuilderImpl<>();
+ }
+
+ public interface Builder {
+ /**
+ * The resolved identity future for this auth scheme.
+ */
+ Builder identity(CompletableFuture extends T> identity);
+
+ /**
+ * The signer to use for this auth scheme.
+ */
+ Builder signer(HttpSigner signer);
+
+ /**
+ * The auth scheme option containing signer and identity properties.
+ */
+ Builder authSchemeOption(AuthSchemeOption authSchemeOption);
+
+ /**
+ * The identity provider that resolved the identity. Used for invalidation on auth errors.
+ */
+ Builder identityProvider(IdentityProvider identityProvider);
+
+ SelectedAuthScheme build();
+ }
+
+ private static final class BuilderImpl implements Builder {
+ private CompletableFuture extends T> identity;
+ private HttpSigner signer;
+ private AuthSchemeOption authSchemeOption;
+ private IdentityProvider identityProvider;
+
+ @Override
+ public Builder identity(CompletableFuture extends T> identity) {
+ this.identity = identity;
+ return this;
+ }
+
+ @Override
+ public Builder signer(HttpSigner signer) {
+ this.signer = signer;
+ return this;
+ }
+
+ @Override
+ public Builder authSchemeOption(AuthSchemeOption authSchemeOption) {
+ this.authSchemeOption = authSchemeOption;
+ return this;
+ }
+
+ @Override
+ public Builder identityProvider(IdentityProvider identityProvider) {
+ this.identityProvider = identityProvider;
+ return this;
+ }
+
+ @Override
+ public SelectedAuthScheme build() {
+ return new SelectedAuthScheme<>(this);
+ }
+ }
}
diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/http/auth/AuthSchemeResolver.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/http/auth/AuthSchemeResolver.java
index 831ad91c408b..51f46a5ffb1b 100644
--- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/http/auth/AuthSchemeResolver.java
+++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/http/auth/AuthSchemeResolver.java
@@ -155,12 +155,12 @@ public static SelectedAuthScheme mergePreExistingAuthSch
AuthSchemeOption.Builder mergedOption = selectedAuthScheme.authSchemeOption().toBuilder();
existingAuthScheme.authSchemeOption().forEachSignerProperty(mergedOption::putSignerPropertyIfAbsent);
existingAuthScheme.authSchemeOption().forEachIdentityProperty(mergedOption::putIdentityPropertyIfAbsent);
- return new SelectedAuthScheme<>(
- selectedAuthScheme.identity(),
- selectedAuthScheme.signer(),
- mergedOption.build(),
- selectedAuthScheme.identityProvider()
- );
+ return SelectedAuthScheme.builder()
+ .identity(selectedAuthScheme.identity())
+ .signer(selectedAuthScheme.signer())
+ .authSchemeOption(mergedOption.build())
+ .identityProvider(selectedAuthScheme.identityProvider())
+ .build();
}
// Start with the freshly resolved auth scheme as the base.
@@ -182,12 +182,12 @@ public void accept(SignerProperty key, S value) {
existingAuthScheme.authSchemeOption().forEachIdentityProperty(mergedOption::putIdentityPropertyIfAbsent);
- return new SelectedAuthScheme<>(
- selectedAuthScheme.identity(),
- selectedAuthScheme.signer(),
- mergedOption.build(),
- selectedAuthScheme.identityProvider()
- );
+ return SelectedAuthScheme.builder()
+ .identity(selectedAuthScheme.identity())
+ .signer(selectedAuthScheme.signer())
+ .authSchemeOption(mergedOption.build())
+ .identityProvider(selectedAuthScheme.identityProvider())
+ .build();
}
/**
@@ -243,10 +243,12 @@ public void accept(SignerProperty key, S value) {
// Only update SELECTED_AUTH_SCHEME if at least one property was re-applied.
if (changed[0]) {
attrs.putAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME,
- new SelectedAuthScheme<>(currentScheme.identity(),
- currentScheme.signer(),
- mergedOption.build(),
- currentScheme.identityProvider()));
+ SelectedAuthScheme.builder()
+ .identity(currentScheme.identity())
+ .signer(currentScheme.signer())
+ .authSchemeOption(mergedOption.build())
+ .identityProvider(currentScheme.identityProvider())
+ .build());
}
}
@@ -284,7 +286,12 @@ private static SelectedAuthScheme trySelectAuthScheme(
CompletableFuture extends T> identity = resolveIdentity(
identityProvider, identityRequestBuilder.build(), metricCollector);
- return new SelectedAuthScheme<>(identity, signer, authOption, identityProvider);
+ return SelectedAuthScheme.builder()
+ .identity(identity)
+ .signer(signer)
+ .authSchemeOption(authOption)
+ .identityProvider(identityProvider)
+ .build();
}
private static CompletableFuture extends T> resolveIdentity(
diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AuthSchemeResolutionStage.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AuthSchemeResolutionStage.java
index 831340b29d16..ae1a2a941c74 100644
--- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AuthSchemeResolutionStage.java
+++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AuthSchemeResolutionStage.java
@@ -151,7 +151,12 @@ private void updateIdentityOnExistingScheme(SelectedAuthSch
}
CompletableFuture extends T> identity = identityProvider.resolveIdentity(ResolveIdentityRequest.builder().build());
executionAttributes.putAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME,
- new SelectedAuthScheme<>(identity, existing.signer(), existing.authSchemeOption()));
+ SelectedAuthScheme.builder()
+ .identity(identity)
+ .signer(existing.signer())
+ .authSchemeOption(existing.authSchemeOption())
+ .identityProvider(identityProvider)
+ .build());
}
private void recordBusinessMetrics(SelectedAuthScheme extends Identity> selectedAuthScheme,
diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/AuthErrorInvalidationHelperTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/AuthErrorInvalidationHelperTest.java
index fa8a2bb1b0c5..62b8b6e4aca9 100644
--- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/AuthErrorInvalidationHelperTest.java
+++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/AuthErrorInvalidationHelperTest.java
@@ -140,12 +140,12 @@ void invalidateIfAuthError_whenIdentityProviderIsNull_doesNotThrow() {
void invalidateIfAuthError_whenInvalidateThrowsException_doesNotPropagate() {
ThrowingIdentityProvider provider = new ThrowingIdentityProvider();
TestIdentity identity = new TestIdentity();
- SelectedAuthScheme selectedAuthScheme = new SelectedAuthScheme<>(
- CompletableFuture.completedFuture(identity),
- mockSigner(),
- AuthSchemeOption.builder().schemeId("test").build(),
- provider
- );
+ SelectedAuthScheme selectedAuthScheme = SelectedAuthScheme.builder()
+ .identity(CompletableFuture.completedFuture(identity))
+ .signer(mockSigner())
+ .authSchemeOption(AuthSchemeOption.builder().schemeId("test").build())
+ .identityProvider(provider)
+ .build();
RequestExecutionContext context = contextWithSelectedAuthScheme(selectedAuthScheme);
Throwable exception = serviceExceptionWithErrorCode("ExpiredToken");
@@ -158,12 +158,12 @@ void invalidateIfAuthError_whenInvalidateThrowsException_doesNotPropagate() {
// --- Helper methods ---
private RequestExecutionContext contextWithProvider(TrackingIdentityProvider provider, TestIdentity identity) {
- SelectedAuthScheme selectedAuthScheme = new SelectedAuthScheme<>(
- CompletableFuture.completedFuture(identity),
- mockSigner(),
- AuthSchemeOption.builder().schemeId("test").build(),
- provider
- );
+ SelectedAuthScheme selectedAuthScheme = SelectedAuthScheme.builder()
+ .identity(CompletableFuture.completedFuture(identity))
+ .signer(mockSigner())
+ .authSchemeOption(AuthSchemeOption.builder().schemeId("test").build())
+ .identityProvider(provider)
+ .build();
return contextWithSelectedAuthScheme(selectedAuthScheme);
}
From 8a6d122c45b3b86d586456025bc02ff9254b1f4b Mon Sep 17 00:00:00 2001
From: Alex Woods
Date: Fri, 10 Jul 2026 09:36:51 -0700
Subject: [PATCH 6/9] Fix types in codegen
---
.../awssdk/codegen/poet/client/ClientClassUtils.java | 5 +----
...st-unsigned-payload-trait-async-client-class.java | 2 +-
...est-unsigned-payload-trait-sync-client-class.java | 2 +-
...-resolve-interceptor-with-endpointsbasedauth.java | 2 +-
...int-resolve-interceptor-with-multiauthsigv4a.java | 2 +-
.../amazon/awssdk/core/SelectedAuthScheme.java | 12 ++++++++++++
6 files changed, 17 insertions(+), 8 deletions(-)
diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/ClientClassUtils.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/ClientClassUtils.java
index 67401329cc01..353593d6d4b6 100644
--- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/ClientClassUtils.java
+++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/ClientClassUtils.java
@@ -564,10 +564,7 @@ static MethodSpec resolveEndpointMethod(AuthSchemeSpecUtils authSchemeSpecUtils,
authSchemeOption.nestedClass("Builder"));
b.addStatement("$1T rs = $1T.create(endpointParams.region().id())", regionSet);
b.addStatement("optionBuilder.putSignerProperty($T.REGION_SET, rs)", awsV4aHttpSigner);
- b.addStatement("selectedAuthScheme = $T.builder().identity(selectedAuthScheme.identity())"
- + ".signer(selectedAuthScheme.signer()).authSchemeOption(optionBuilder.build())"
- + ".identityProvider(selectedAuthScheme.identityProvider()).build()",
- SelectedAuthScheme.class);
+ b.addStatement("selectedAuthScheme = selectedAuthScheme.toBuilder().authSchemeOption(optionBuilder.build()).build()");
b.endControlFlow();
}
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-unsigned-payload-trait-async-client-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-unsigned-payload-trait-async-client-class.java
index 0283255249f7..4d224bbe73a3 100644
--- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-unsigned-payload-trait-async-client-class.java
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-unsigned-payload-trait-async-client-class.java
@@ -1080,7 +1080,7 @@ private Endpoint resolveEndpoint(SdkRequest request, ExecutionAttributes executi
AuthSchemeOption.Builder optionBuilder = selectedAuthScheme.authSchemeOption().toBuilder();
RegionSet rs = RegionSet.create(endpointParams.region().id());
optionBuilder.putSignerProperty(AwsV4aHttpSigner.REGION_SET, rs);
- selectedAuthScheme = SelectedAuthScheme.builder().identity(selectedAuthScheme.identity()).signer(selectedAuthScheme.signer()).authSchemeOption(optionBuilder.build()).identityProvider(selectedAuthScheme.identityProvider()).build();
+ selectedAuthScheme = selectedAuthScheme.toBuilder().authSchemeOption(optionBuilder.build()).build();
}
executionAttributes.putAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME, selectedAuthScheme);
}
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-unsigned-payload-trait-sync-client-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-unsigned-payload-trait-sync-client-class.java
index 57be531f36ea..1050590421f9 100644
--- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-unsigned-payload-trait-sync-client-class.java
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-unsigned-payload-trait-sync-client-class.java
@@ -965,7 +965,7 @@ private Endpoint resolveEndpoint(SdkRequest request, ExecutionAttributes executi
AuthSchemeOption.Builder optionBuilder = selectedAuthScheme.authSchemeOption().toBuilder();
RegionSet rs = RegionSet.create(endpointParams.region().id());
optionBuilder.putSignerProperty(AwsV4aHttpSigner.REGION_SET, rs);
- selectedAuthScheme = SelectedAuthScheme.builder().identity(selectedAuthScheme.identity()).signer(selectedAuthScheme.signer()).authSchemeOption(optionBuilder.build()).identityProvider(selectedAuthScheme.identityProvider()).build();
+ selectedAuthScheme = selectedAuthScheme.toBuilder().authSchemeOption(optionBuilder.build()).build();
}
executionAttributes.putAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME, selectedAuthScheme);
}
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-endpointsbasedauth.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-endpointsbasedauth.java
index 9e95995a17de..944d03995440 100644
--- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-endpointsbasedauth.java
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-endpointsbasedauth.java
@@ -81,7 +81,7 @@ public SdkRequest modifyRequest(Context.ModifyRequest context, ExecutionAttribut
AuthSchemeOption.Builder optionBuilder = selectedAuthScheme.authSchemeOption().toBuilder();
RegionSet regionSet = RegionSet.create(endpointParams.region().id());
optionBuilder.putSignerProperty(AwsV4aHttpSigner.REGION_SET, regionSet);
- selectedAuthScheme = SelectedAuthScheme.builder().identity(selectedAuthScheme.identity()).signer(selectedAuthScheme.signer()).authSchemeOption(optionBuilder.build()).identityProvider(selectedAuthScheme.identityProvider()).build();
+ selectedAuthScheme = selectedAuthScheme.toBuilder().authSchemeOption(optionBuilder.build()).build();
}
executionAttributes.putAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME, selectedAuthScheme);
}
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-multiauthsigv4a.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-multiauthsigv4a.java
index 884e9ca4eda9..5faf70df4a08 100644
--- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-multiauthsigv4a.java
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-multiauthsigv4a.java
@@ -70,7 +70,7 @@ public SdkRequest modifyRequest(Context.ModifyRequest context, ExecutionAttribut
AuthSchemeOption.Builder optionBuilder = selectedAuthScheme.authSchemeOption().toBuilder();
RegionSet regionSet = RegionSet.create(endpointParams.region().id());
optionBuilder.putSignerProperty(AwsV4aHttpSigner.REGION_SET, regionSet);
- selectedAuthScheme = SelectedAuthScheme.builder().identity(selectedAuthScheme.identity()).signer(selectedAuthScheme.signer()).authSchemeOption(optionBuilder.build()).identityProvider(selectedAuthScheme.identityProvider()).build();
+ selectedAuthScheme = selectedAuthScheme.toBuilder().authSchemeOption(optionBuilder.build()).build();
}
executionAttributes.putAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME, selectedAuthScheme);
}
diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/SelectedAuthScheme.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/SelectedAuthScheme.java
index 232aae6aca25..26f1645692d0 100644
--- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/SelectedAuthScheme.java
+++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/SelectedAuthScheme.java
@@ -88,6 +88,18 @@ public IdentityProvider identityProvider() {
return identityProvider;
}
+ /**
+ * Returns a builder initialized with the values from this instance, allowing modification
+ * of individual fields while preserving the rest.
+ */
+ public Builder toBuilder() {
+ return new BuilderImpl()
+ .identity(this.identity)
+ .signer(this.signer)
+ .authSchemeOption(this.authSchemeOption)
+ .identityProvider(this.identityProvider);
+ }
+
public static Builder builder() {
return new BuilderImpl<>();
}
From 9b561b54319a7ae39440568207370f76e400a704 Mon Sep 17 00:00:00 2001
From: Alex Woods
Date: Fri, 10 Jul 2026 10:23:19 -0700
Subject: [PATCH 7/9] Fix failing SSO test
---
.../sso/auth/SsoProfileCredentialsProviderFactoryTest.java | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/services/sso/src/test/java/software/amazon/awssdk/services/sso/auth/SsoProfileCredentialsProviderFactoryTest.java b/services/sso/src/test/java/software/amazon/awssdk/services/sso/auth/SsoProfileCredentialsProviderFactoryTest.java
index d6956ac87022..e40017020be5 100644
--- a/services/sso/src/test/java/software/amazon/awssdk/services/sso/auth/SsoProfileCredentialsProviderFactoryTest.java
+++ b/services/sso/src/test/java/software/amazon/awssdk/services/sso/auth/SsoProfileCredentialsProviderFactoryTest.java
@@ -46,6 +46,7 @@
import software.amazon.awssdk.auth.credentials.ProfileProviderCredentialsContext;
import software.amazon.awssdk.auth.token.credentials.SdkToken;
import software.amazon.awssdk.auth.token.credentials.SdkTokenProvider;
+import software.amazon.awssdk.core.exception.SdkServiceException;
import software.amazon.awssdk.profiles.ProfileFile;
import software.amazon.awssdk.services.sso.SsoClient;
import software.amazon.awssdk.services.sso.auth.ExpiredTokenException;
@@ -561,7 +562,10 @@ public void tokenProviderThrowsOnSecondCall_staleCachedCredentialsReturned() {
when(mockSsoClient.getRoleCredentials(Mockito.any(GetRoleCredentialsRequest.class)))
.thenReturn(GetRoleCredentialsResponse.builder().roleCredentials(roleCredentials).build());
- RuntimeException secondCallError = new RuntimeException("Token refresh failed on second attempt");
+ RuntimeException secondCallError = SdkServiceException.builder()
+ .message("SSO service unavailable")
+ .statusCode(500)
+ .build();
when(sdkTokenProvider.resolveToken())
.thenReturn(SsoAccessToken.builder().accessToken("valid-token").expiresAt(Instant.now().plusSeconds(3600)).build())
.thenThrow(secondCallError);
From 6f0444da8e5e14e01abc5ebfebe3701d5727d393 Mon Sep 17 00:00:00 2001
From: Alex Woods
Date: Fri, 10 Jul 2026 11:48:41 -0700
Subject: [PATCH 8/9] PR Cleanups
---
.../ContainerCredentialsProvider.java | 5 +-
.../InstanceProfileCredentialsProvider.java | 5 +-
.../ProcessCredentialsProvider.java | 9 +-
.../awssdk/awscore/internal/AwsErrorCode.java | 2 +-
.../awssdk/identity/spi/IdentityProvider.java | 6 +-
.../core/exception/SdkServiceException.java | 2 +-
.../utils/AuthErrorInvalidationHelper.java | 4 +-
.../signin/auth/LoginCredentialsProvider.java | 5 +-
.../sso/auth/SsoCredentialsProvider.java | 5 +-
.../sts/auth/StsCredentialsProvider.java | 5 +-
.../AuthErrorInvalidationIntegrationTest.java | 321 ------------------
.../awssdk/utils/cache/CacheRefreshUtils.java | 2 +-
.../awssdk/utils/cache/CachedSupplier.java | 16 +-
13 files changed, 30 insertions(+), 357 deletions(-)
delete mode 100644 test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/retry/AuthErrorInvalidationIntegrationTest.java
diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ContainerCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ContainerCredentialsProvider.java
index 9a95620ecb60..ff8d1b170467 100644
--- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ContainerCredentialsProvider.java
+++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ContainerCredentialsProvider.java
@@ -191,9 +191,8 @@ public AwsCredentials resolveCredentials() {
@Override
public CompletableFuture invalidate(AwsCredentialsIdentity identity) {
String rejectedAccessKeyId = identity.accessKeyId();
- return CompletableFuture.runAsync(() ->
- credentialsCache.invalidate(cachedCreds -> rejectedAccessKeyId.equals(cachedCreds.accessKeyId()))
- );
+ credentialsCache.invalidate(cachedCreds -> rejectedAccessKeyId.equals(cachedCreds.accessKeyId()));
+ return CompletableFuture.completedFuture(null);
}
@Override
diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java
index 19baaff757fe..67c09cceac1b 100644
--- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java
+++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java
@@ -172,9 +172,8 @@ public AwsCredentials resolveCredentials() {
@Override
public CompletableFuture