Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import software.amazon.awssdk.utils.Validate;
import software.amazon.awssdk.utils.builder.CopyableBuilder;
import software.amazon.awssdk.utils.builder.ToCopyableBuilder;
import software.amazon.awssdk.utils.cache.CacheRefreshUtils;
import software.amazon.awssdk.utils.cache.CachedSupplier;
import software.amazon.awssdk.utils.cache.NonBlocking;
import software.amazon.awssdk.utils.cache.RefreshResult;
Expand Down Expand Up @@ -87,7 +88,6 @@ public final class ContainerCredentialsProvider
private static final List<String> VALID_LOOP_BACK_IPV6 = Arrays.asList(EKS_CONTAINER_HOST_IPV6);

private static final Duration DEFAULT_STALE_TIME = Duration.ofMinutes(1);
private static final Duration DEFAULT_PREFETCH_TIME = Duration.ofMinutes(5);

private final String endpoint;
private final HttpCredentialsLoader httpCredentialsLoader;
Expand All @@ -114,9 +114,11 @@ private ContainerCredentialsProvider(BuilderImpl builder) {
: builder.sourceChain + "," + PROVIDER_NAME;
this.httpCredentialsLoader = HttpCredentialsLoader.create(this.providerName);
this.staleTime = Optional.ofNullable(builder.staleTime).orElse(DEFAULT_STALE_TIME);
this.prefetchTime = Optional.ofNullable(builder.prefetchTime).orElse(DEFAULT_PREFETCH_TIME);
Validate.isTrue(this.staleTime.compareTo(this.prefetchTime) <= 0,
"staleTime (%s) must be less than or equal to prefetchTime (%s).", this.staleTime, this.prefetchTime);
this.prefetchTime = builder.prefetchTime;
if (this.prefetchTime != null) {
Validate.isTrue(this.staleTime.compareTo(this.prefetchTime) <= 0,
"staleTime (%s) must be less than or equal to prefetchTime (%s).", this.staleTime, this.prefetchTime);
}

if (Boolean.TRUE.equals(builder.asyncCredentialUpdateEnabled)) {
Validate.paramNotBlank(builder.asyncThreadName, "asyncThreadName");
Expand Down Expand Up @@ -172,7 +174,11 @@ private Instant prefetchTime(Instant expiration) {
if (expiration == null) {
return Instant.now().plus(1, ChronoUnit.HOURS);
}
return expiration.minus(prefetchTime);

Instant now = Instant.now();
Duration effectivePrefetchWindow = CacheRefreshUtils.computePrefetchWindow(expiration, prefetchTime, now);

return expiration.minus(effectivePrefetchWindow);
}

@Override
Expand Down Expand Up @@ -356,7 +362,10 @@ public interface Builder extends HttpCredentialsProvider.Builder<ContainerCreden
* <p>This value must be greater than or equal to {@link #staleTime(Duration)}. Setting this equal to
* {@code staleTime} effectively disables prefetch, causing all refreshes to be mandatory (blocking).
*
* <p>By default, this is 5 minutes.
* <p>If not explicitly set, the advisory refresh window is computed dynamically based on the credential's
* remaining lifetime: 5 minutes for credentials with less than 20 minutes remaining, 15 minutes for 20-90
* minutes remaining, and 60 minutes for 90+ minutes remaining. This dynamic window is recomputed on each
* successful refresh.
*
* @param prefetchTime the duration before expiration that triggers advisory (proactive) refresh
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import software.amazon.awssdk.utils.Validate;
import software.amazon.awssdk.utils.builder.CopyableBuilder;
import software.amazon.awssdk.utils.builder.ToCopyableBuilder;
import software.amazon.awssdk.utils.cache.CacheRefreshUtils;
import software.amazon.awssdk.utils.cache.CachedSupplier;
import software.amazon.awssdk.utils.cache.NonBlocking;
import software.amazon.awssdk.utils.cache.RefreshResult;
Expand Down Expand Up @@ -122,9 +123,11 @@ private InstanceProfileCredentialsProvider(BuilderImpl builder) {
.build();

this.staleTime = Validate.getOrDefault(builder.staleTime, () -> Duration.ofMinutes(1));
this.prefetchTime = Validate.getOrDefault(builder.prefetchTime, () -> Duration.ofMinutes(5));
Validate.isTrue(this.staleTime.compareTo(this.prefetchTime) <= 0,
"staleTime (%s) must be less than or equal to prefetchTime (%s).", this.staleTime, this.prefetchTime);
this.prefetchTime = builder.prefetchTime;
if (this.prefetchTime != null) {
Validate.isTrue(this.staleTime.compareTo(this.prefetchTime) <= 0,
"staleTime (%s) must be less than or equal to prefetchTime (%s).", this.staleTime, this.prefetchTime);
}

if (Boolean.TRUE.equals(builder.asyncCredentialUpdateEnabled)) {
Validate.paramNotBlank(builder.asyncThreadName, "asyncThreadName");
Expand Down Expand Up @@ -208,12 +211,14 @@ private Instant prefetchTime(Instant expiration) {
return null;
}

// Advisory refresh window: use configured prefetchTime before expiry.
// If remaining lifetime < prefetchTime, refresh immediately.
if (timeUntilExpiration.compareTo(prefetchTime) < 0) {
// Use dynamic window when user has not explicitly configured prefetchTime
Duration effectivePrefetchWindow = CacheRefreshUtils.computePrefetchWindow(expiration, prefetchTime, now);

// If remaining lifetime < the advisory window, refresh immediately.
if (timeUntilExpiration.compareTo(effectivePrefetchWindow) < 0) {
return now;
}
return expiration.minus(prefetchTime);
return expiration.minus(effectivePrefetchWindow);
}

@Override
Expand Down Expand Up @@ -391,7 +396,10 @@ public interface Builder extends HttpCredentialsProvider.Builder<InstanceProfile
* <p>This value must be greater than or equal to {@link #staleTime(Duration)}. Setting this equal to
* {@code staleTime} effectively disables prefetch, causing all refreshes to be mandatory (blocking).
*
* <p>By default, this is 5 minutes.
* <p>If not explicitly set, the advisory refresh window is computed dynamically based on the credential's
* remaining lifetime: 5 minutes for credentials with less than 20 minutes remaining, 15 minutes for 20-90
* minutes remaining, and 60 minutes for 90+ minutes remaining. This dynamic window is recomputed on each
* successful refresh.
*
* @param duration the duration before expiration that triggers advisory (proactive) refresh
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import software.amazon.awssdk.utils.Validate;
import software.amazon.awssdk.utils.builder.CopyableBuilder;
import software.amazon.awssdk.utils.builder.ToCopyableBuilder;
import software.amazon.awssdk.utils.cache.CacheRefreshUtils;
import software.amazon.awssdk.utils.cache.CachedSupplier;
import software.amazon.awssdk.utils.cache.NonBlocking;
import software.amazon.awssdk.utils.cache.RefreshResult;
Expand Down Expand Up @@ -85,7 +86,6 @@ public final class ProcessCredentialsProvider
.removeErrorLocations(true)
.build();
private static final Duration DEFAULT_STALE_TIME = Duration.ofMinutes(1);
private static final Duration DEFAULT_PREFETCH_TIME = Duration.ofMinutes(5);

private final List<String> executableCommand;
private final long processOutputLimit;
Expand Down Expand Up @@ -119,9 +119,11 @@ private ProcessCredentialsProvider(Builder builder) {
? PROVIDER_NAME
: builder.sourceChain + "," + PROVIDER_NAME;
this.staleTime = Optional.ofNullable(builder.staleTime).orElse(DEFAULT_STALE_TIME);
this.prefetchTime = Optional.ofNullable(builder.prefetchTime).orElse(DEFAULT_PREFETCH_TIME);
Validate.isTrue(this.staleTime.compareTo(this.prefetchTime) <= 0,
"staleTime (%s) must be less than or equal to prefetchTime (%s).", this.staleTime, this.prefetchTime);
this.prefetchTime = builder.prefetchTime;
if (this.prefetchTime != null) {
Validate.isTrue(this.staleTime.compareTo(this.prefetchTime) <= 0,
"staleTime (%s) must be less than or equal to prefetchTime (%s).", this.staleTime, this.prefetchTime);
}

CachedSupplier.Builder<AwsCredentials> cacheBuilder = CachedSupplier.builder(this::refreshCredentials)
.cachedValueName(toString());
Expand Down Expand Up @@ -194,7 +196,9 @@ private Instant prefetchTime(Instant expiration) {
if (expiration == null || expiration.equals(Instant.MAX)) {
return Instant.MAX;
}
return expiration.minus(prefetchTime);
Instant now = Instant.now();
Duration dynamicWindow = CacheRefreshUtils.computePrefetchWindow(expiration, prefetchTime, now);
return expiration.minus(dynamicWindow);
}

/**
Expand Down Expand Up @@ -382,7 +386,10 @@ public Builder staleTime(Duration staleTime) {
* <p>This value must be greater than or equal to {@link #staleTime(Duration)}. Setting this equal to
* {@code staleTime} effectively disables prefetch, causing all refreshes to be mandatory (blocking).
*
* <p>By default, this is 5 minutes.</p>
* <p>If not explicitly set, the advisory refresh window is computed dynamically based on the credential's
* remaining lifetime: 5 minutes for credentials with less than 20 minutes remaining, 15 minutes for 20-90
* minutes remaining, and 60 minutes for 90+ minutes remaining. This dynamic window is recomputed on each
* successful refresh.</p>
*
* @param prefetchTime the duration before expiration that triggers advisory (proactive) refresh
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,10 @@ public interface Builder extends CopyableBuilder<Builder, WebIdentityTokenFileCr
* <p>This value must be greater than or equal to {@link #staleTime(Duration)}. Setting this equal to
* {@code staleTime} effectively disables prefetch, causing all refreshes to be mandatory (blocking).
*
* <p>By default, this is 5 minutes.
* <p>If not explicitly set, the advisory refresh window is computed dynamically based on the credential's
* remaining lifetime: 5 minutes for credentials with less than 20 minutes remaining, 15 minutes for 20-90
* minutes remaining, and 60 minutes for 90+ minutes remaining. This dynamic window is recomputed on each
* successful refresh.
*
* @param prefetchTime the duration before expiration that triggers advisory (proactive) refresh
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -639,13 +639,13 @@ void imdsCallFrequencyIsLimited() {
stubSecureCredentialsResponse(aResponse().withBody(successfulCredentialsResponse1));
AwsCredentials credentialsAtStart = credentialsProvider.resolveCredentials();

// Move time forward but still before the prefetch window (5 min before expiry).
// Since prefetchTime = expiration - 5min = now + 5h55m, anything before that should not trigger refresh.
clock.time = now.plus(5, HOURS);
// Move time forward but still before the prefetch window (60 min before expiry for 6h credentials).
// Since dynamic prefetchTime = expiration - 60min = now + 5h, anything before that should not trigger refresh.
clock.time = now.plus(4, HOURS);
stubSecureCredentialsResponse(aResponse().withBody(successfulCredentialsResponse2));
AwsCredentials credentials5HoursLater = credentialsProvider.resolveCredentials();
AwsCredentials credentialsLater = credentialsProvider.resolveCredentials();

assertThat(credentials5HoursLater).isEqualTo(credentialsAtStart);
assertThat(credentialsLater).isEqualTo(credentialsAtStart);
assertThat(credentialsAtStart.secretAccessKey()).isEqualTo("SECRET_ACCESS_KEY");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import software.amazon.awssdk.utils.Validate;
import software.amazon.awssdk.utils.builder.CopyableBuilder;
import software.amazon.awssdk.utils.builder.ToCopyableBuilder;
import software.amazon.awssdk.utils.cache.CacheRefreshUtils;
import software.amazon.awssdk.utils.cache.CachedSupplier;
import software.amazon.awssdk.utils.cache.NonBlocking;
import software.amazon.awssdk.utils.cache.RefreshResult;
Expand Down Expand Up @@ -78,7 +79,6 @@ public final class LoginCredentialsProvider implements
private static final String PROVIDER_NAME = BusinessMetricFeatureId.CREDENTIALS_LOGIN.value();

private static final Duration DEFAULT_STALE_TIME = Duration.ofMinutes(1);
private static final Duration DEFAULT_PREFETCH_TIME = Duration.ofMinutes(5);
private static final Path DEFAULT_TOKEN_LOCATION = Paths.get(userHomeDirectory(), ".aws", "login", "cache");

private static final String ASYNC_THREAD_NAME = "sdk-login-credentials-provider";
Expand Down Expand Up @@ -106,9 +106,11 @@ private LoginCredentialsProvider(BuilderImpl builder) {
this.loginSession = paramNotBlank(builder.loginSession, "LoginSession");

this.staleTime = Optional.ofNullable(builder.staleTime).orElse(DEFAULT_STALE_TIME);
this.prefetchTime = Optional.ofNullable(builder.prefetchTime).orElse(DEFAULT_PREFETCH_TIME);
Validate.isTrue(this.staleTime.compareTo(this.prefetchTime) <= 0,
"staleTime (%s) must be less than or equal to prefetchTime (%s).", this.staleTime, this.prefetchTime);
this.prefetchTime = builder.prefetchTime;
if (this.prefetchTime != null) {
Validate.isTrue(this.staleTime.compareTo(this.prefetchTime) <= 0,
"staleTime (%s) must be less than or equal to prefetchTime (%s).", this.staleTime, this.prefetchTime);
}
this.sourceChain = builder.sourceChain;

this.providerName = StringUtils.isEmpty(builder.sourceChain)
Expand Down Expand Up @@ -136,21 +138,22 @@ private LoginCredentialsProvider(BuilderImpl builder) {
}

/**
* Update the expiring session SSO credentials by calling SSO. Invoked by {@link CachedSupplier} when the credentials are
* close to expiring.
* Update the expiring session Login credentials by calling the Signin Service. Invoked by {@link CachedSupplier} when the
* credentials are close to expiring.
*/
private RefreshResult<AwsCredentials> updateSigninCredentials() {
// always re-load token from the disk in case it has been updated elsewhere
LoginAccessToken tokenFromDisc = onDiskTokenManager.loadToken().orElseThrow(
() -> SdkClientException.create("Token cache file for login_session `" + loginSession + "` not found. "
() -> new InvalidTokenException("Token cache file for login_session `" + loginSession + "` not found. "
+ "You must re-authenticate."));

Instant currentExpirationTime = tokenFromDisc.getAccessToken().expirationTime().orElseThrow(
() -> SdkClientException.create("Invalid token expiration time. You must re-authenticate.")
() -> new InvalidTokenException("Invalid token expiration time. You must re-authenticate.")
);

Duration effectivePrefetch = CacheRefreshUtils.computePrefetchWindow(currentExpirationTime, prefetchTime, Instant.now());
if (shouldNotRefresh(currentExpirationTime, staleTime)
&& shouldNotRefresh(currentExpirationTime, prefetchTime)) {
&& shouldNotRefresh(currentExpirationTime, effectivePrefetch)) {
log.debug(() -> "Using access token from disk, current expiration time is : " + currentExpirationTime);
AwsCredentials credentials = tokenFromDisc.getAccessToken()
.toBuilder()
Expand All @@ -159,7 +162,7 @@ && shouldNotRefresh(currentExpirationTime, prefetchTime)) {

return RefreshResult.builder(credentials)
.staleTime(currentExpirationTime.minus(staleTime))
.prefetchTime(currentExpirationTime.minus(prefetchTime))
.prefetchTime(currentExpirationTime.minus(effectivePrefetch))
.build();
}

Expand Down Expand Up @@ -203,7 +206,8 @@ private RefreshResult<AwsCredentials> refreshFromSigninService(LoginAccessToken

return RefreshResult.builder((AwsCredentials) updatedCredentials)
.staleTime(newExpiration.minus(staleTime))
.prefetchTime(newExpiration.minus(prefetchTime))
.prefetchTime(newExpiration.minus(
CacheRefreshUtils.computePrefetchWindow(newExpiration, prefetchTime, Instant.now())))
.build();
} catch (AccessDeniedException accessDeniedException) {
if (accessDeniedException.error() == null) {
Expand Down Expand Up @@ -232,11 +236,18 @@ private RefreshResult<AwsCredentials> refreshFromSigninService(LoginAccessToken

/**
* Determines whether a given exception represents a non-recoverable refresh failure that should bypass
* static stability. For Login, this is an {@link AccessDeniedException} with error code
* {@link OAuth2ErrorCode#TOKEN_EXPIRED}, {@link OAuth2ErrorCode#USER_CREDENTIALS_CHANGED},
* or {@link OAuth2ErrorCode#INSUFFICIENT_PERMISSIONS}.
* static stability. For Login, this includes:
* <ul>
* <li>Missing or invalid token cache on disk (requires re-authentication)</li>
* <li>An {@link AccessDeniedException} with error code {@link OAuth2ErrorCode#TOKEN_EXPIRED},
* {@link OAuth2ErrorCode#USER_CREDENTIALS_CHANGED}, or {@link OAuth2ErrorCode#INSUFFICIENT_PERMISSIONS}</li>
* </ul>
*/
private static boolean isCacheInvalidating(RuntimeException e) {
if (e instanceof InvalidTokenException) {
return true;
}

AccessDeniedException ade = extractAccessDeniedException(e);
if (ade == null) {
return false;
Expand Down Expand Up @@ -355,7 +366,10 @@ public interface Builder extends CopyableBuilder<Builder, LoginCredentialsProvid
* <p>This value must be greater than or equal to {@link #staleTime(Duration)}. Setting this equal to
* {@code staleTime} effectively disables prefetch, causing all refreshes to be mandatory (blocking).
*
* <p>By default, this is 5 minutes.
* <p>If not explicitly set, the advisory refresh window is computed dynamically based on the credential's
* remaining lifetime: 5 minutes for credentials with less than 20 minutes remaining, 15 minutes for 20-90
* minutes remaining, and 60 minutes for 90+ minutes remaining. This dynamic window is recomputed on each
* successful refresh.
*
* @param prefetchTime the duration before expiration that triggers advisory (proactive) refresh
*/
Expand Down Expand Up @@ -386,6 +400,17 @@ public interface Builder extends CopyableBuilder<Builder, LoginCredentialsProvid
LoginCredentialsProvider build();
}

/**
* Exception indicating that the cached login token is missing or invalid, requiring the user to re-authenticate.
* This exception bypasses static stability (cache extension on failure) because there is no valid token state
* to fall back to.
*/
private static final class InvalidTokenException extends SdkClientException {
private InvalidTokenException(String message) {
super(builder().message(message));
}
}

protected static final class BuilderImpl implements Builder {
private Boolean asyncCredentialUpdateEnabled = true;
private SigninClient signinClient;
Expand Down
Loading
Loading