2222import java .nio .file .Path ;
2323import java .nio .file .Paths ;
2424import java .time .Duration ;
25+ import java .time .Instant ;
2526import java .util .Optional ;
2627import software .amazon .awssdk .annotations .SdkPublicApi ;
2728import software .amazon .awssdk .annotations .ThreadSafe ;
2829import software .amazon .awssdk .auth .credentials .AwsCredentials ;
2930import software .amazon .awssdk .auth .credentials .AwsCredentialsProvider ;
31+ import software .amazon .awssdk .auth .credentials .AwsSessionCredentials ;
32+ import software .amazon .awssdk .core .exception .SdkClientException ;
3033import software .amazon .awssdk .core .useragent .BusinessMetricFeatureId ;
3134import 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 ;
3239import 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 ;
3343import software .amazon .awssdk .utils .SdkAutoCloseable ;
3444import software .amazon .awssdk .utils .StringUtils ;
3545import software .amazon .awssdk .utils .builder .CopyableBuilder ;
5666public 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 (shouldNotRefresh (currentExpirationTime , staleTime )
142+ && shouldNotRefresh (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,16 @@ public Builder toBuilder() {
152238 return new BuilderImpl (this );
153239 }
154240
241+
242+ /**
243+ *
244+ * @return true if the token does NOT need to be refreshed - it is after the given refresh window, eg stale/prefetch time.
245+ */
246+ private static boolean shouldNotRefresh (Instant expiration , Duration refreshWindow ) {
247+ Instant now = Instant .now ();
248+ return expiration .isAfter (now .plus (refreshWindow ));
249+ }
250+
155251 /**
156252 * A builder for creating a custom {@link LoginCredentialsProvider}.
157253 */
0 commit comments