Skip to content

Commit 64e9344

Browse files
committed
Implement credentials refresh for LoginCredentialsProvider
1 parent ec6e648 commit 64e9344

File tree

8 files changed

+992
-4
lines changed

8 files changed

+992
-4
lines changed

services/signin/src/main/java/software/amazon/awssdk/services/signin/auth/LoginCredentialsProvider.java

Lines changed: 94 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,24 @@
2222
import java.nio.file.Path;
2323
import java.nio.file.Paths;
2424
import java.time.Duration;
25+
import java.time.Instant;
2526
import java.util.Optional;
2627
import software.amazon.awssdk.annotations.SdkPublicApi;
2728
import software.amazon.awssdk.annotations.ThreadSafe;
2829
import software.amazon.awssdk.auth.credentials.AwsCredentials;
2930
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
31+
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
32+
import software.amazon.awssdk.core.exception.SdkClientException;
3033
import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId;
3134
import software.amazon.awssdk.services.signin.SigninClient;
35+
import software.amazon.awssdk.services.signin.internal.AccessTokenManager;
36+
import software.amazon.awssdk.services.signin.internal.LoginAccessToken;
37+
import software.amazon.awssdk.services.signin.internal.LoginCacheDirectorySystemSetting;
38+
import software.amazon.awssdk.services.signin.internal.OnDiskTokenManager;
3239
import software.amazon.awssdk.services.signin.model.CreateOAuth2TokenRequest;
40+
import software.amazon.awssdk.services.signin.model.CreateOAuth2TokenResponse;
41+
import software.amazon.awssdk.services.signin.model.SigninException;
42+
import software.amazon.awssdk.utils.Logger;
3343
import software.amazon.awssdk.utils.SdkAutoCloseable;
3444
import software.amazon.awssdk.utils.StringUtils;
3545
import software.amazon.awssdk.utils.builder.CopyableBuilder;
@@ -56,6 +66,8 @@
5666
public class LoginCredentialsProvider implements
5767
AwsCredentialsProvider, SdkAutoCloseable,
5868
ToCopyableBuilder<LoginCredentialsProvider.Builder, LoginCredentialsProvider> {
69+
private static final Logger log = Logger.loggerFor(LoginCredentialsProvider.class);
70+
5971
private static final String PROVIDER_NAME = BusinessMetricFeatureId.CREDENTIALS_LOGIN.value();
6072

6173
private static final Duration DEFAULT_STALE_TIME = Duration.ofMinutes(1);
@@ -74,10 +86,15 @@ public class LoginCredentialsProvider implements
7486
private final Path tokenCacheLocation;
7587

7688
private final CachedSupplier<AwsCredentials> credentialCache;
89+
private final AccessTokenManager onDiskTokenManager;
7790

7891
private final Boolean asyncCredentialUpdateEnabled;
7992

80-
public LoginCredentialsProvider(BuilderImpl builder) {
93+
/**
94+
*
95+
* @see #builder()
96+
*/
97+
private LoginCredentialsProvider(BuilderImpl builder) {
8198
this.signinClient = notNull(builder.signinClient, "SigninClient must not be null.");
8299
this.loginSession = paramNotBlank(builder.loginSession, "LoginSession");
83100

@@ -94,6 +111,8 @@ public LoginCredentialsProvider(BuilderImpl builder) {
94111
.map(p -> Paths.get(p))
95112
.orElse(DEFAULT_TOKEN_LOCATION));
96113

114+
this.onDiskTokenManager = OnDiskTokenManager.create(this.tokenCacheLocation, this.loginSession);
115+
97116
this.asyncCredentialUpdateEnabled = builder.asyncCredentialUpdateEnabled;
98117
CachedSupplier.Builder<AwsCredentials> cacheBuilder =
99118
CachedSupplier.builder(this::updateSigninCredentials)
@@ -110,8 +129,75 @@ public LoginCredentialsProvider(BuilderImpl builder) {
110129
* close to expiring.
111130
*/
112131
private RefreshResult<AwsCredentials> updateSigninCredentials() {
113-
// TODO: Future PRs will implement this
114-
throw new UnsupportedOperationException();
132+
// always re-load token from the disk in case it has been updated elsewhere
133+
LoginAccessToken tokenFromDisc = onDiskTokenManager.loadToken().orElseThrow(
134+
() -> SdkClientException.create("Token cache file for login_session `" + loginSession + "` not found. "
135+
+ "You must re-authenticate."));
136+
137+
Instant currentExpirationTime = tokenFromDisc.getAccessToken().expirationTime().orElseThrow(
138+
() -> SdkClientException.create("Invalid token expiration time. You must re-authenticate.")
139+
);
140+
141+
if (isWithinRefreshWindow(currentExpirationTime, staleTime)
142+
&& isWithinRefreshWindow(currentExpirationTime, prefetchTime)) {
143+
log.debug(() -> "Using access token from disk, current expiration time is : " + currentExpirationTime);
144+
AwsCredentials credentials = tokenFromDisc.getAccessToken()
145+
.toBuilder()
146+
.providerName(this.providerName)
147+
.build();
148+
149+
return RefreshResult.builder(credentials)
150+
.staleTime(currentExpirationTime.minus(staleTime))
151+
.prefetchTime(currentExpirationTime.minus(prefetchTime))
152+
.build();
153+
}
154+
155+
return refreshFromSigninService(tokenFromDisc);
156+
}
157+
158+
private RefreshResult<AwsCredentials> refreshFromSigninService(LoginAccessToken tokenFromDisc) {
159+
log.debug(() -> "Credentials are near expiration/expired, refreshing from Signin service.");
160+
161+
try {
162+
CreateOAuth2TokenRequest refreshRequest =
163+
CreateOAuth2TokenRequest
164+
.builder()
165+
.tokenInput(t -> t
166+
.clientId(tokenFromDisc.getClientId())
167+
.refreshToken(tokenFromDisc.getRefreshToken())
168+
.grantType("refresh_token"))
169+
.dpopProof("TODO") // TODO: This will be implemented in a separate PR
170+
.build();
171+
172+
CreateOAuth2TokenResponse createTokenResponse = signinClient.createOAuth2Token(refreshRequest);
173+
174+
Instant newExpiration = Instant.now().plusSeconds(createTokenResponse.tokenOutput().expiresIn());
175+
AwsSessionCredentials updatedCredentials = AwsSessionCredentials
176+
.builder()
177+
.accessKeyId(createTokenResponse.tokenOutput().accessToken().accessKeyId())
178+
.secretAccessKey(createTokenResponse.tokenOutput().accessToken().secretAccessKey())
179+
.sessionToken(createTokenResponse.tokenOutput().accessToken().sessionToken())
180+
.accountId(tokenFromDisc.getAccessToken().accountId().orElseThrow(
181+
() -> SdkClientException.create("Invalid access token, missing account ID. You must re-authenticate.")
182+
))
183+
.expirationTime(newExpiration)
184+
.providerName(this.providerName)
185+
.build();
186+
187+
onDiskTokenManager.storeToken(tokenFromDisc.toBuilder()
188+
.accessToken(updatedCredentials)
189+
.refreshToken(createTokenResponse.tokenOutput().refreshToken())
190+
.build());
191+
192+
return RefreshResult.builder((AwsCredentials) updatedCredentials)
193+
.staleTime(newExpiration.minus(staleTime))
194+
.prefetchTime(newExpiration.minus(prefetchTime))
195+
.build();
196+
} catch (SigninException serviceException) {
197+
throw SdkClientException.create(
198+
"Unable to refresh AWS Signin Access Token: You must re-authenticate.",
199+
serviceException);
200+
}
115201
}
116202

117203
/**
@@ -152,6 +238,11 @@ public Builder toBuilder() {
152238
return new BuilderImpl(this);
153239
}
154240

241+
private static boolean isWithinRefreshWindow(Instant expiration, Duration staleTime) {
242+
Instant now = Instant.now();
243+
return expiration.isAfter(now.plus(staleTime));
244+
}
245+
155246
/**
156247
* A builder for creating a custom {@link LoginCredentialsProvider}.
157248
*/
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.services.signin.internal;
17+
18+
import java.util.Optional;
19+
import software.amazon.awssdk.annotations.SdkInternalApi;
20+
import software.amazon.awssdk.utils.SdkAutoCloseable;
21+
22+
@SdkInternalApi
23+
public interface AccessTokenManager extends SdkAutoCloseable {
24+
Optional<LoginAccessToken> loadToken();
25+
26+
void storeToken(LoginAccessToken token);
27+
}

0 commit comments

Comments
 (0)