4848import software .amazon .awssdk .auth .token .credentials .SdkTokenProvider ;
4949import software .amazon .awssdk .profiles .ProfileFile ;
5050import software .amazon .awssdk .services .sso .SsoClient ;
51+ import software .amazon .awssdk .services .sso .auth .ExpiredTokenException ;
5152import software .amazon .awssdk .services .sso .internal .SsoAccessToken ;
5253import software .amazon .awssdk .services .sso .internal .SsoAccessTokenProvider ;
5354import software .amazon .awssdk .services .sso .model .GetRoleCredentialsRequest ;
@@ -172,6 +173,8 @@ private static Stream<Arguments> ssoErrorValues() {
172173
173174 @ Test
174175 public void tokenResolvedFromTokenProvider (@ Mock SdkTokenProvider sdkTokenProvider ){
176+ SsoClient mockSsoClient = mock (SsoClient .class );
177+
175178 ProfileFile profileFile = configFile ("[profile test]\n " +
176179 "sso_account_id=accountId\n " +
177180 "sso_role_name=roleName\n " +
@@ -182,20 +185,25 @@ public void tokenResolvedFromTokenProvider(@Mock SdkTokenProvider sdkTokenProvid
182185 "sso_start_url=https//d-abc123.awsapps.com/start" );
183186 SsoProfileCredentialsProviderFactory factory = new SsoProfileCredentialsProviderFactory ();
184187 when (sdkTokenProvider .resolveToken ()).thenReturn (SsoAccessToken .builder ().accessToken ("sample" ).expiresAt (Instant .now ()).build ());
188+
189+ RoleCredentials roleCredentials = RoleCredentials .builder ()
190+ .accessKeyId ("AKID" )
191+ .secretAccessKey ("secret" )
192+ .sessionToken ("session" )
193+ .expiration (Instant .now ().minusSeconds (1 ).toEpochMilli ())
194+ .build ();
195+ when (mockSsoClient .getRoleCredentials (Mockito .any (GetRoleCredentialsRequest .class )))
196+ .thenReturn (GetRoleCredentialsResponse .builder ().roleCredentials (roleCredentials ).build ());
197+
185198 AwsCredentialsProvider credentialsProvider = factory .create (ProfileProviderCredentialsContext .builder ()
186199 .profile (profileFile .profile ("test" ).get ())
187200 .profileFile (profileFile )
188- .build (), sdkTokenProvider );
189- // Call resolveCredentials() twice to verify token is re-resolved on each call
190- for (int i = 0 ; i < 2 ; i ++) {
191- try {
192- credentialsProvider .resolveCredentials ();
193- } catch (Exception e ) {
194- // sso client created internally which cannot be mocked.
195- }
196- }
197- // The first call triggers the supplier (which calls resolveToken()), and the second call
198- // also triggers the supplier since credentials from the first call expired immediately.
201+ .build (),
202+ sdkTokenProvider ,
203+ mockSsoClient );
204+ credentialsProvider .resolveCredentials ();
205+ credentialsProvider .resolveCredentials ();
206+
199207 Mockito .verify (sdkTokenProvider , times (2 )).resolveToken ();
200208 }
201209
@@ -306,6 +314,7 @@ public void validProfileWithTokenProvider_createsProviderSuccessfully() {
306314 @ Test
307315 public void tokenIsReResolvedOnEachCredentialRefresh () {
308316 int numberOfRefreshCalls = 3 ;
317+ SsoClient mockSsoClient = mock (SsoClient .class );
309318
310319 ProfileFile profileFile = configFile ("[profile test]\n " +
311320 "sso_account_id=accountId\n " +
@@ -323,22 +332,26 @@ public void tokenIsReResolvedOnEachCredentialRefresh() {
323332 SsoAccessToken .builder ().accessToken ("token-4" ).expiresAt (Instant .now ().plusSeconds (3600 )).build ()
324333 );
325334
335+ RoleCredentials roleCredentials = RoleCredentials .builder ()
336+ .accessKeyId ("AKID" )
337+ .secretAccessKey ("secret" )
338+ .sessionToken ("session" )
339+ .expiration (Instant .now ().minusSeconds (1 ).toEpochMilli ())
340+ .build ();
341+ when (mockSsoClient .getRoleCredentials (Mockito .any (GetRoleCredentialsRequest .class )))
342+ .thenReturn (GetRoleCredentialsResponse .builder ().roleCredentials (roleCredentials ).build ());
343+
326344 SsoProfileCredentialsProviderFactory factory = new SsoProfileCredentialsProviderFactory ();
327345 AwsCredentialsProvider credentialsProvider = factory .create (
328346 ProfileProviderCredentialsContext .builder ()
329347 .profile (profileFile .profile ("test" ).get ())
330348 .profileFile (profileFile )
331349 .build (),
332- sdkTokenProvider );
350+ sdkTokenProvider ,
351+ mockSsoClient );
333352
334- // Call resolveCredentials() multiple times to trigger the supplier
335353 for (int i = 0 ; i < numberOfRefreshCalls ; i ++) {
336- try {
337- credentialsProvider .resolveCredentials ();
338- } catch (Exception e ) {
339- // Expected: SsoClient created internally cannot reach the SSO service.
340- // The supplier IS still invoked before the SsoClient call fails.
341- }
354+ credentialsProvider .resolveCredentials ();
342355 }
343356
344357 Mockito .verify (sdkTokenProvider , Mockito .atLeast (numberOfRefreshCalls )).resolveToken ();
@@ -464,16 +477,9 @@ public void errorPropagation_resolveTokenThrowsUncheckedIOException_propagatesTo
464477 sdkTokenProvider );
465478
466479 assertThatThrownBy (credentialsProvider ::resolveCredentials )
467- .isInstanceOfAny (UncheckedIOException .class , RuntimeException .class )
468- .satisfies (thrown -> {
469- // The error must surface - either directly or wrapped
470- if (thrown instanceof UncheckedIOException ) {
471- assertThat (thrown .getCause ().getMessage ()).contains ("Token file not found" );
472- } else {
473- // May be wrapped in another exception; verify the root cause is present
474- assertThat (thrown ).hasRootCauseMessage ("Token file not found" );
475- }
476- });
480+ .isInstanceOf (ExpiredTokenException .class )
481+ .hasMessageContaining ("expired or is otherwise invalid" )
482+ .hasCauseInstanceOf (UncheckedIOException .class );
477483 }
478484
479485 @ Test
@@ -499,22 +505,9 @@ public void errorPropagation_resolveTokenThrowsRuntimeException_propagatesToCall
499505 sdkTokenProvider );
500506
501507 assertThatThrownBy (credentialsProvider ::resolveCredentials )
502- .isInstanceOf (RuntimeException .class )
503- .satisfies (thrown -> {
504- // The error must surface - verify the original message is reachable
505- boolean messageFound = false ;
506- Throwable current = thrown ;
507- while (current != null ) {
508- if (current .getMessage () != null && current .getMessage ().contains ("Token is expired" )) {
509- messageFound = true ;
510- break ;
511- }
512- current = current .getCause ();
513- }
514- assertThat (messageFound )
515- .as ("Expected 'Token is expired' somewhere in the exception chain" )
516- .isTrue ();
517- });
508+ .isInstanceOf (ExpiredTokenException .class )
509+ .hasMessageContaining ("expired or is otherwise invalid" )
510+ .hasCauseInstanceOf (RuntimeException .class );
518511 }
519512
520513 @ Test
@@ -541,15 +534,15 @@ public void errorPropagation_resolveTokenReturnsNullTokenValue_errorPropagates()
541534 .build (),
542535 sdkTokenProvider );
543536
544- // The null token value should cause an error when resolveCredentials is called.
545- // This may manifest as NullPointerException, SdkClientException, or similar.
546- // The critical assertion is that the error is NOT silently swallowed.
547537 assertThatThrownBy (credentialsProvider ::resolveCredentials )
548- .isInstanceOf (Exception .class );
538+ .isInstanceOf (ExpiredTokenException .class )
539+ .hasMessageContaining ("expired or is otherwise invalid" );
549540 }
550541
551542 @ Test
552- public void errorPropagation_tokenProviderThrowsOnSecondCall_errorPropagates () {
543+ public void tokenProviderThrowsOnSecondCall_staleCachedCredentialsReturned () {
544+ SsoClient mockSsoClient = mock (SsoClient .class );
545+
553546 ProfileFile profileFile = configFile ("[profile test]\n " +
554547 "sso_account_id=accountId\n " +
555548 "sso_role_name=roleName\n " +
@@ -559,6 +552,15 @@ public void errorPropagation_tokenProviderThrowsOnSecondCall_errorPropagates() {
559552 "sso_region=region\n " +
560553 "sso_start_url=https//d-abc123.awsapps.com/start" );
561554
555+ RoleCredentials roleCredentials = RoleCredentials .builder ()
556+ .accessKeyId ("AKID" )
557+ .secretAccessKey ("secret" )
558+ .sessionToken ("session" )
559+ .expiration (Instant .now ().minusSeconds (1 ).toEpochMilli ())
560+ .build ();
561+ when (mockSsoClient .getRoleCredentials (Mockito .any (GetRoleCredentialsRequest .class )))
562+ .thenReturn (GetRoleCredentialsResponse .builder ().roleCredentials (roleCredentials ).build ());
563+
562564 RuntimeException secondCallError = new RuntimeException ("Token refresh failed on second attempt" );
563565 when (sdkTokenProvider .resolveToken ())
564566 .thenReturn (SsoAccessToken .builder ().accessToken ("valid-token" ).expiresAt (Instant .now ().plusSeconds (3600 )).build ())
@@ -570,35 +572,15 @@ public void errorPropagation_tokenProviderThrowsOnSecondCall_errorPropagates() {
570572 .profile (profileFile .profile ("test" ).get ())
571573 .profileFile (profileFile )
572574 .build (),
573- sdkTokenProvider );
575+ sdkTokenProvider ,
576+ mockSsoClient );
574577
575- // First call: token resolves successfully, but SsoClient call will fail
576- // (since it's a real client with no endpoint)
577- try {
578- credentialsProvider .resolveCredentials ();
579- } catch (Exception e ) {
580- // Expected: internal SsoClient cannot reach the SSO service
581- }
578+ // First call succeeds and caches credentials
579+ credentialsProvider .resolveCredentials ();
582580
583- // Second call: token provider throws, this error must propagate
584- assertThatThrownBy (credentialsProvider ::resolveCredentials )
585- .isInstanceOf (RuntimeException .class )
586- .satisfies (thrown -> {
587- // Verify the second call's error surfaces somewhere in the chain
588- boolean messageFound = false ;
589- Throwable current = thrown ;
590- while (current != null ) {
591- if (current .getMessage () != null &&
592- current .getMessage ().contains ("Token refresh failed on second attempt" )) {
593- messageFound = true ;
594- break ;
595- }
596- current = current .getCause ();
597- }
598- assertThat (messageFound )
599- .as ("Expected 'Token refresh failed on second attempt' in the exception chain" )
600- .isTrue ();
601- });
581+ // Second call: token provider throws InvalidTokenException, but CachedSupplier with
582+ // StaleValueBehavior.ALLOW returns stale cached credentials (static stability)
583+ assertThat (credentialsProvider .resolveCredentials ()).isNotNull ();
602584 }
603585
604586 @ Test
0 commit comments