Skip to content

Commit dcf9d6e

Browse files
authored
Merge pull request #4475 from aws/alexwoo/master/loginCredentialProvider_errorUpdates
Error handling/message updates + updates from surface area api review
2 parents 1c1169b + 475c3c1 commit dcf9d6e

File tree

7 files changed

+113
-43
lines changed

7 files changed

+113
-43
lines changed

services/signin/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,10 @@
5656
<artifactId>http-auth-aws</artifactId>
5757
<version>${awsjavasdk.version}</version>
5858
</dependency>
59+
<dependency>
60+
<groupId>software.amazon.awssdk</groupId>
61+
<artifactId>profiles</artifactId>
62+
<version>${awsjavasdk.version}</version>
63+
</dependency>
5964
</dependencies>
6065
</project>

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

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import java.time.Duration;
2525
import java.time.Instant;
2626
import java.util.Optional;
27+
import software.amazon.awssdk.annotations.NotThreadSafe;
28+
import software.amazon.awssdk.annotations.SdkInternalApi;
2729
import software.amazon.awssdk.annotations.SdkPublicApi;
2830
import software.amazon.awssdk.annotations.ThreadSafe;
2931
import software.amazon.awssdk.auth.credentials.AwsCredentials;
@@ -38,9 +40,9 @@
3840
import software.amazon.awssdk.services.signin.internal.LoginAccessToken;
3941
import software.amazon.awssdk.services.signin.internal.LoginCacheDirectorySystemSetting;
4042
import software.amazon.awssdk.services.signin.internal.OnDiskTokenManager;
43+
import software.amazon.awssdk.services.signin.model.AccessDeniedException;
4144
import software.amazon.awssdk.services.signin.model.CreateOAuth2TokenRequest;
4245
import software.amazon.awssdk.services.signin.model.CreateOAuth2TokenResponse;
43-
import software.amazon.awssdk.services.signin.model.SigninException;
4446
import software.amazon.awssdk.utils.Logger;
4547
import software.amazon.awssdk.utils.SdkAutoCloseable;
4648
import software.amazon.awssdk.utils.StringUtils;
@@ -55,17 +57,17 @@
5557
* It periodically sends a {@link CreateOAuth2TokenRequest} to the AWS
5658
* Sign-On Service to refresh short-lived sessions to use for authentication. These sessions are updated using a single
5759
* calling thread (by default) or asynchronously (if {@link Builder#asyncCredentialUpdateEnabled(Boolean)} is set).
58-
*
60+
* <p>
5961
* If the credentials are not successfully updated before expiration, calls to {@link #resolveCredentials()} will block until
6062
* they are updated successfully.
61-
*
63+
* <p>
6264
* Users of this provider must {@link #close()} it when they are finished using it.
63-
*
65+
* <p>
6466
* This is created using {@link LoginCredentialsProvider#builder()}.
6567
*/
6668
@SdkPublicApi
6769
@ThreadSafe
68-
public class LoginCredentialsProvider implements
70+
public final class LoginCredentialsProvider implements
6971
AwsCredentialsProvider, SdkAutoCloseable,
7072
ToCopyableBuilder<LoginCredentialsProvider.Builder, LoginCredentialsProvider> {
7173
private static final Logger log = Logger.loggerFor(LoginCredentialsProvider.class);
@@ -196,10 +198,32 @@ private RefreshResult<AwsCredentials> refreshFromSigninService(LoginAccessToken
196198
.staleTime(newExpiration.minus(staleTime))
197199
.prefetchTime(newExpiration.minus(prefetchTime))
198200
.build();
199-
} catch (SigninException serviceException) {
200-
throw SdkClientException.create(
201-
"Unable to refresh AWS Signin Access Token: You must re-authenticate.",
202-
serviceException);
201+
} catch (AccessDeniedException accessDeniedException) {
202+
if (accessDeniedException.error() == null) {
203+
throw accessDeniedException;
204+
}
205+
206+
switch (accessDeniedException.error()) {
207+
case TOKEN_EXPIRED:
208+
throw SdkClientException.create(
209+
"Your session has expired. Please reauthenticate.",
210+
accessDeniedException);
211+
case USER_CREDENTIALS_CHANGED:
212+
throw SdkClientException.create(
213+
"Unable to refresh credentials because of a change in your password. "
214+
+ "Please reauthenticate with your new password.",
215+
accessDeniedException
216+
);
217+
case INSUFFICIENT_PERMISSIONS:
218+
throw SdkClientException.create(
219+
"Unable to refresh credentials due to insufficient permissions. You may be missing permission "
220+
+ "for the 'CreateOAuth2Token' action.",
221+
accessDeniedException
222+
);
223+
default:
224+
throw accessDeniedException;
225+
226+
}
203227
}
204228
}
205229

@@ -222,7 +246,7 @@ public Duration prefetchTime() {
222246
/**
223247
* Get a builder for creating a custom {@link LoginCredentialsProvider}.
224248
*/
225-
public static BuilderImpl builder() {
249+
public static Builder builder() {
226250
return new BuilderImpl();
227251
}
228252

@@ -243,7 +267,6 @@ public Builder toBuilder() {
243267

244268

245269
/**
246-
*
247270
* @return true if the token does NOT need to be refreshed - it is after the given refresh window, eg stale/prefetch time.
248271
*/
249272
private static boolean shouldNotRefresh(Instant expiration, Duration refreshWindow) {
@@ -254,6 +277,7 @@ private static boolean shouldNotRefresh(Instant expiration, Duration refreshWind
254277
/**
255278
* A builder for creating a custom {@link LoginCredentialsProvider}.
256279
*/
280+
@NotThreadSafe
257281
public interface Builder extends CopyableBuilder<Builder, LoginCredentialsProvider> {
258282
/**
259283
* Configure the {@link SigninClient} to use when calling Signin to update the session. This client should not be shut
@@ -265,15 +289,15 @@ public interface Builder extends CopyableBuilder<Builder, LoginCredentialsProvid
265289
* Configure whether the provider should fetch credentials asynchronously in the background. If this is true, threads are
266290
* less likely to block when credentials are loaded, but additional resources are used to maintain the provider.
267291
*
268-
* <p>By default, this is disabled.</p>
292+
* <p>By default, this is enabled.
269293
*/
270294
Builder asyncCredentialUpdateEnabled(Boolean asyncCredentialUpdateEnabled);
271295

272296
/**
273-
* Configure the amount of time, relative to signin token expiration, that the cached credentials are considered stale and
297+
* Configure the amount of time, relative to login token expiration, that the cached credentials are considered stale and
274298
* should no longer be used. All threads will block until the value is updated.
275299
*
276-
* <p>By default, this is 1 minute.</p>
300+
* <p>By default, this is 1 minute.
277301
*/
278302
Builder staleTime(Duration staleTime);
279303

@@ -284,7 +308,7 @@ public interface Builder extends CopyableBuilder<Builder, LoginCredentialsProvid
284308
* Prefetch updates will occur between the specified time and the stale time of the provider. Prefetch updates may be
285309
* asynchronous. See {@link #asyncCredentialUpdateEnabled}.
286310
*
287-
* <p>By default, this is 5 minutes.</p>
311+
* <p>By default, this is 5 minutes.
288312
*/
289313
Builder prefetchTime(Duration prefetchTime);
290314

@@ -303,19 +327,18 @@ public interface Builder extends CopyableBuilder<Builder, LoginCredentialsProvid
303327
* An optional string denoting previous credentials providers that are chained with this one. This method is primarily
304328
* intended for use by AWS SDK internal components and should not be used directly by external users.
305329
*/
330+
@SdkInternalApi
306331
Builder sourceChain(String sourceChain);
307332

308333
/**
309334
* Create a {@link LoginCredentialsProvider} using the configuration applied to this builder.
310-
*
311-
* @return
312335
*/
313336
@Override
314337
LoginCredentialsProvider build();
315338
}
316339

317340
protected static final class BuilderImpl implements Builder {
318-
private Boolean asyncCredentialUpdateEnabled = false;
341+
private Boolean asyncCredentialUpdateEnabled = true;
319342
private SigninClient signinClient;
320343
private Duration staleTime;
321344
private Duration prefetchTime;

services/signin/src/main/java/software/amazon/awssdk/services/signin/internal/DpopAuthPlugin.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import software.amazon.awssdk.annotations.SdkInternalApi;
2222
import software.amazon.awssdk.core.SdkPlugin;
2323
import software.amazon.awssdk.core.SdkServiceClientConfiguration;
24+
import software.amazon.awssdk.core.exception.SdkClientException;
2425
import software.amazon.awssdk.http.auth.spi.scheme.AuthSchemeOption;
2526
import software.amazon.awssdk.http.auth.spi.scheme.AuthSchemeProvider;
2627
import software.amazon.awssdk.identity.spi.IdentityProvider;
@@ -63,7 +64,11 @@ public void configureClient(SdkServiceClientConfiguration.Builder config) {
6364
// the refresh request takes the clientId/refreshToken sourced from the access token on disk as input
6465
// so we must sign the request with the dpopKey loaded from the same load. IE: do not read the
6566
// access token file twice!
66-
scb.putAuthScheme(DpopAuthScheme.create(StaticDpopIdentityProvider.create(dpopKeyPem)));
67+
try {
68+
scb.putAuthScheme(DpopAuthScheme.create(StaticDpopIdentityProvider.create(dpopKeyPem)));
69+
} catch (Exception e) {
70+
throw SdkClientException.create("Unable to refresh Login credentials. Please reauthenticate ", e);
71+
}
6772
}
6873

6974
private static class DpopAuthSchemeProvider implements SigninAuthSchemeProvider {

services/signin/src/main/java/software/amazon/awssdk/services/signin/internal/EcKeyLoader.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,6 @@ private static class ParsedEcKey {
121121
byte[] publicBytes;
122122
}
123123

124-
125124
/**
126125
* Follows the SEC1 / RFC 5915 ASN.1 format: PrivateKeyInfo ::= SEQUENCE { version INTEGER (0), privateKeyAlgorithm
127126
* AlgorithmIdentifier, -- ecPublicKey + curve OID privateKey OCTET STRING -- contains the SEC1 DER parameters [0]

services/signin/src/main/java/software/amazon/awssdk/services/signin/internal/OnDiskTokenManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ private OnDiskTokenManager(Path cacheLocation, String loginSession) {
5454

5555
private String deriveCacheKey(String loginSession) {
5656
try {
57-
MessageDigest sha1 = MessageDigest.getInstance("sha256");
57+
MessageDigest sha1 = MessageDigest.getInstance("SHA-256");
5858
sha1.update(loginSession.getBytes(StandardCharsets.UTF_8));
5959
return BinaryUtils.toHex(sha1.digest()).toLowerCase(Locale.ENGLISH);
6060
} catch (NoSuchAlgorithmException e) {

services/signin/src/test/java/software/amazon/awssdk/services/signin/auth/LoginCredentialsProviderTest.java

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,7 @@
2020
import static org.junit.jupiter.api.Assertions.assertNotNull;
2121
import static org.junit.jupiter.api.Assertions.assertThrows;
2222
import static org.junit.jupiter.api.Assertions.assertTrue;
23-
import static org.mockito.ArgumentMatchers.any;
24-
import static org.mockito.Mockito.mock;
25-
import static org.mockito.Mockito.never;
26-
import static org.mockito.Mockito.times;
27-
import static org.mockito.Mockito.verify;
28-
import static org.mockito.Mockito.when;
23+
2924
import static software.amazon.awssdk.services.signin.auth.internal.DpopTestUtils.VALID_TEST_PEM;
3025
import static software.amazon.awssdk.services.signin.auth.internal.DpopTestUtils.getJwtPayloadFromEncodedDpopHeader;
3126
import static software.amazon.awssdk.services.signin.auth.internal.DpopTestUtils.verifySignature;
@@ -41,10 +36,8 @@
4136
import org.junit.jupiter.api.BeforeEach;
4237
import org.junit.jupiter.api.Test;
4338
import org.junit.jupiter.api.io.TempDir;
44-
import org.mockito.ArgumentCaptor;
4539
import software.amazon.awssdk.auth.credentials.AwsCredentials;
4640
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
47-
import software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute;
4841
import software.amazon.awssdk.core.SdkRequest;
4942
import software.amazon.awssdk.core.exception.SdkClientException;
5043
import software.amazon.awssdk.core.interceptor.Context;
@@ -61,10 +54,10 @@
6154
import software.amazon.awssdk.services.signin.internal.LoginAccessToken;
6255
import software.amazon.awssdk.services.signin.internal.OnDiskTokenManager;
6356
import software.amazon.awssdk.services.signin.model.CreateOAuth2TokenRequest;
64-
import software.amazon.awssdk.services.signin.model.CreateOAuth2TokenResponse;
57+
import software.amazon.awssdk.services.signin.model.OAuth2ErrorCode;
6558
import software.amazon.awssdk.services.signin.model.SigninException;
66-
import software.amazon.awssdk.testutils.service.http.MockAsyncHttpClient;
6759
import software.amazon.awssdk.testutils.service.http.MockSyncHttpClient;
60+
import software.amazon.awssdk.utils.StringInputStream;
6861

6962
public class LoginCredentialsProviderTest {
7063
private static final String LOGIN_SESSION_ID = "loginSessionId";
@@ -188,7 +181,7 @@ public void resolveCredentials_whenCredentialsExpired_refreshesAndUpdatesCache()
188181
}
189182

190183
@Test
191-
public void resolveCredentials_whenCredentialsExpired_serviceCallFails_raisesException() {
184+
public void resolveCredentials_whenCredentialsExpired_serviceCallFailsWithGeneric500_raisesException() {
192185
// expired
193186
AwsSessionCredentials creds = buildCredentials(Instant.now().minusSeconds(60));
194187
LoginAccessToken token = buildAccessToken(creds);
@@ -199,7 +192,43 @@ public void resolveCredentials_whenCredentialsExpired_serviceCallFails_raisesExc
199192
.response(SdkHttpResponse.builder().statusCode(500).build())
200193
.build()
201194
);
202-
assertThrows(SdkClientException.class, () -> loginCredentialsProvider.resolveCredentials());
195+
assertThrows(SigninException.class, () -> loginCredentialsProvider.resolveCredentials());
196+
}
197+
198+
@Test
199+
public void resolveCredentials_whenCredentialsExpired_serviceCallFailsWithTokenExpired_raisesException() {
200+
// expired
201+
AwsSessionCredentials creds = buildCredentials(Instant.now().minusSeconds(60));
202+
LoginAccessToken token = buildAccessToken(creds);
203+
tokenManager.storeToken(token);
204+
205+
stubAccessDeniedException(OAuth2ErrorCode.TOKEN_EXPIRED);
206+
SdkClientException e = assertThrows(SdkClientException.class, () -> loginCredentialsProvider.resolveCredentials());
207+
assertTrue(e.getMessage().contains("Your session has expired"));
208+
}
209+
210+
@Test
211+
public void resolveCredentials_whenCredentialsExpired_serviceCallFailsWithUserExpired_raisesException() {
212+
// expired
213+
AwsSessionCredentials creds = buildCredentials(Instant.now().minusSeconds(60));
214+
LoginAccessToken token = buildAccessToken(creds);
215+
tokenManager.storeToken(token);
216+
217+
stubAccessDeniedException(OAuth2ErrorCode.USER_CREDENTIALS_CHANGED);
218+
SdkClientException e = assertThrows(SdkClientException.class, () -> loginCredentialsProvider.resolveCredentials());
219+
assertTrue(e.getMessage().contains("change in your password"));
220+
}
221+
222+
@Test
223+
public void resolveCredentials_whenCredentialsExpired_serviceCallFailsWithInsufficentPermissions_raisesException() {
224+
// expired
225+
AwsSessionCredentials creds = buildCredentials(Instant.now().minusSeconds(60));
226+
LoginAccessToken token = buildAccessToken(creds);
227+
tokenManager.storeToken(token);
228+
229+
stubAccessDeniedException(OAuth2ErrorCode.INSUFFICIENT_PERMISSIONS);
230+
SdkClientException e = assertThrows(SdkClientException.class, () -> loginCredentialsProvider.resolveCredentials());
231+
assertTrue(e.getMessage().contains("insufficient permissions"));
203232
}
204233

205234
private static void verifyResolvedCredentialsAreUpdated(AwsCredentials resolvedCredentials) {
@@ -254,6 +283,22 @@ private void stubSuccessfulRefreshResponse() {
254283
);
255284
}
256285

286+
private void stubAccessDeniedException(OAuth2ErrorCode errorCode) {
287+
String errorBody = "{\"error\":\"" + errorCode + "\",\"message\":\"The refresh token has expired.\"}";
288+
mockHttpClient.stubNextResponse(
289+
HttpExecuteResponse
290+
.builder()
291+
.response(
292+
SdkHttpResponse
293+
.builder()
294+
.putHeader("X-Amzn-Errortype", "AccessDeniedException")
295+
.statusCode(401)
296+
.build())
297+
.responseBody(AbortableInputStream.create(new StringInputStream(errorBody)))
298+
.build()
299+
);
300+
}
301+
257302
private AwsSessionCredentials buildCredentials(Instant expirationTime) {
258303
return AwsSessionCredentials.builder()
259304
.accessKeyId("akid")

services/signin/src/test/java/software/amazon/awssdk/services/signin/auth/internal/OnDiskTokenManagerTest.java

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -114,16 +114,9 @@ void loadToken_whenCorruptJson_raisesException() throws IOException {
114114

115115
@Test
116116
void storeToken_whenIoFails_raisesException() {
117-
Path readOnlyDir = tempDir.resolve("readonly");
118-
try {
119-
Files.createDirectory(readOnlyDir);
120-
readOnlyDir.toFile().setReadOnly();
121-
} catch (IOException e) {
122-
fail("Unable to set up readonly dir");
123-
}
124-
117+
Path readOnlyDir = tempDir.resolve("pathdoesnotexist");
125118
OnDiskTokenManager manager = OnDiskTokenManager.create(readOnlyDir, LOGIN_SESSION_ID);
126-
assertThrows(SdkClientException.class, () -> manager.storeToken(token));
119+
SdkClientException e = assertThrows(SdkClientException.class, () -> manager.storeToken(token));
127120
}
128121

129122
@Test
@@ -145,7 +138,7 @@ void unmarshalToken_whenMissingRequiredFields_raisesException() throws IOExcepti
145138

146139
private Path tokenLocation(String loginSession) {
147140
try {
148-
MessageDigest sha1 = MessageDigest.getInstance("sha256");
141+
MessageDigest sha1 = MessageDigest.getInstance("SHA-256");
149142
sha1.update(loginSession.getBytes(StandardCharsets.UTF_8));
150143
String cacheKey = BinaryUtils.toHex(sha1.digest()).toLowerCase(Locale.ENGLISH);
151144
return tempDir.resolve(cacheKey + ".json");

0 commit comments

Comments
 (0)