diff --git a/.changes/next-release/bugfix-AWSSTS-bc7dfee.json b/.changes/next-release/bugfix-AWSSTS-bc7dfee.json new file mode 100644 index 000000000000..a13984403e22 --- /dev/null +++ b/.changes/next-release/bugfix-AWSSTS-bc7dfee.json @@ -0,0 +1,6 @@ +{ + "type": "bugfix", + "category": "AWS STS", + "contributor": "", + "description": "Fix `StsWebIdentityTokenFileCredentialsProvider` not respecting custom `prefetchTime` and `staleTime` configurations." +} 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 e19a6bf4b9f6..74ebc39c664e 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 @@ -230,5 +230,13 @@ public B prefetchTime(Duration prefetchTime) { public T build() { return providerConstructor.apply((B) this); } + + /** + * Whether the provider should fetch credentials asynchronously in the background. + *
By default, this is false.
+ */ + Boolean asyncCredentialUpdateEnabled() { + return asyncCredentialUpdateEnabled; + } } } diff --git a/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsWebIdentityTokenFileCredentialsProvider.java b/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsWebIdentityTokenFileCredentialsProvider.java index cf853fd9a5bf..389f4415bdd9 100644 --- a/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsWebIdentityTokenFileCredentialsProvider.java +++ b/services/sts/src/main/java/software/amazon/awssdk/services/sts/auth/StsWebIdentityTokenFileCredentialsProvider.java @@ -106,11 +106,19 @@ private StsWebIdentityTokenFileCredentialsProvider(Builder builder) { .assumeRoleWithWebIdentityRequest(assumeRoleWithWebIdentityRequest.get()) .webIdentityTokenFile(credentialProperties.webIdentityTokenFile()) .build(); - credentialsProviderLocal = + + StsAssumeRoleWithWebIdentityCredentialsProvider.Builder providerBuilder = StsAssumeRoleWithWebIdentityCredentialsProvider.builder() .stsClient(builder.stsClient) .refreshRequest(supplier) - .build(); + .staleTime(this.staleTime()) + .prefetchTime(this.prefetchTime()); + + if (builder.asyncCredentialUpdateEnabled() != null) { + providerBuilder.asyncCredentialUpdateEnabled(builder.asyncCredentialUpdateEnabled()); + } + + credentialsProviderLocal = providerBuilder.build(); } catch (RuntimeException e) { // If we couldn't load the credentials provider for some reason, save an exception describing why. This exception // will only be raised on calls to getCredentials. We don't want to raise an exception here because it may be @@ -181,7 +189,7 @@ private Builder() { } private Builder(StsWebIdentityTokenFileCredentialsProvider provider) { - super(StsWebIdentityTokenFileCredentialsProvider::new); + super(StsWebIdentityTokenFileCredentialsProvider::new, provider); this.roleArn = provider.roleArn; this.roleSessionName = provider.roleSessionName; this.webIdentityTokenFile = provider.webIdentityTokenFile; diff --git a/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsWebIdentityTokenCredentialProviderTest.java b/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsWebIdentityTokenCredentialProviderTest.java index adfa9e4a8fd9..058eff163eff 100644 --- a/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsWebIdentityTokenCredentialProviderTest.java +++ b/services/sts/src/test/java/software/amazon/awssdk/services/sts/auth/StsWebIdentityTokenCredentialProviderTest.java @@ -27,13 +27,16 @@ import software.amazon.awssdk.services.sts.StsClient; import software.amazon.awssdk.services.sts.model.AssumeRoleWithWebIdentityRequest; import software.amazon.awssdk.services.sts.model.AssumeRoleWithWebIdentityResponse; +import software.amazon.awssdk.services.sts.model.AssumedRoleUser; import software.amazon.awssdk.services.sts.model.Credentials; import java.nio.file.Paths; +import java.time.Duration; import java.time.Instant; import software.amazon.awssdk.testutils.EnvironmentVariableHelper; import static org.mockito.Mockito.when; +import static org.assertj.core.api.Assertions.assertThat; @ExtendWith(MockitoExtension.class) class StsWebIdentityTokenCredentialProviderTest { @@ -111,4 +114,108 @@ void createAssumeRoleWithWebIdentityTokenCredentialsProvider_raisesInResolveCred // exception should be raised lazily when resolving credentials, not at creation time. Assert.assertThrows(IllegalStateException.class, provider::resolveCredentials); } + + @Test + void customPrefetchTime_actuallyTriggersRefreshEarly() throws InterruptedException { + Mockito.reset(stsClient); + + Instant tokenExpiration = Instant.now().plusSeconds(5); + Duration customPrefetchTime = Duration.ofSeconds(2); + Duration customStaleTime = Duration.ofSeconds(1); + + when(stsClient.assumeRoleWithWebIdentity(Mockito.any(AssumeRoleWithWebIdentityRequest.class))) + .thenReturn(AssumeRoleWithWebIdentityResponse.builder() + .credentials(Credentials.builder() + .accessKeyId("key1") + .secretAccessKey("secret1") + .sessionToken("session1") + .expiration(tokenExpiration) + .build()) + .assumedRoleUser(AssumedRoleUser.builder() + .arn("arn:aws:iam::123456789012:role/test-role") + .assumedRoleId("role:session") + .build()) + .build()) + + .thenReturn(AssumeRoleWithWebIdentityResponse.builder() + .credentials(Credentials.builder() + .accessKeyId("key2") + .secretAccessKey("secret2") + .sessionToken("session2") + .expiration(Instant.now().plusSeconds(8)) + .build()) + .assumedRoleUser(AssumedRoleUser.builder() + .arn("arn:aws:iam::123456789012:role/test-role") + .assumedRoleId("role:session") + .build()) + .build()); + + + StsWebIdentityTokenFileCredentialsProvider provider = + StsWebIdentityTokenFileCredentialsProvider.builder() + .stsClient(stsClient) + .asyncCredentialUpdateEnabled(true) + .prefetchTime(customPrefetchTime) + .staleTime(customStaleTime) + .build(); + + try { + assertThat(provider.prefetchTime()).isEqualTo(customPrefetchTime); + assertThat(provider.staleTime()).isEqualTo(customStaleTime); + + provider.resolveCredentials(); + Mockito.verify(stsClient, Mockito.times(1)).assumeRoleWithWebIdentity(Mockito.any(AssumeRoleWithWebIdentityRequest.class)); + + // Wait 5 seconds to ensure prefetch completes + Thread.sleep(5_000); + Mockito.verify(stsClient, Mockito.times(2)).assumeRoleWithWebIdentity(Mockito.any(AssumeRoleWithWebIdentityRequest.class)); + + } finally { + provider.close(); + } + } + + @Test + void defaultTiming_usesStandardValues() { + StsWebIdentityTokenFileCredentialsProvider provider = + StsWebIdentityTokenFileCredentialsProvider.builder() + .stsClient(stsClient) + .build(); + + try { + assertThat(provider.prefetchTime()).isEqualTo(Duration.ofMinutes(5)); + assertThat(provider.staleTime()).isEqualTo(Duration.ofMinutes(1)); + } finally { + provider.close(); + } + } + + @Test + void toBuilder_preservesCustomTimingConfiguration() { + Duration customPrefetch = Duration.ofMinutes(10); + Duration customStale = Duration.ofMinutes(3); + + StsWebIdentityTokenFileCredentialsProvider originalProvider = + StsWebIdentityTokenFileCredentialsProvider.builder() + .stsClient(stsClient) + .prefetchTime(customPrefetch) + .staleTime(customStale) + .build(); + + try { + assertThat(originalProvider.prefetchTime()).isEqualTo(customPrefetch); + assertThat(originalProvider.staleTime()).isEqualTo(customStale); + + StsWebIdentityTokenFileCredentialsProvider copiedProvider = originalProvider.toBuilder().build(); + + try { + assertThat(copiedProvider.prefetchTime()).isEqualTo(customPrefetch); + assertThat(copiedProvider.staleTime()).isEqualTo(customStale); + } finally { + copiedProvider.close(); + } + } finally { + originalProvider.close(); + } + } } \ No newline at end of file