diff --git a/aws-android-sdk-cognitoidentityprovider/src/main/java/com/amazonaws/mobileconnectors/cognitoidentityprovider/CognitoUser.java b/aws-android-sdk-cognitoidentityprovider/src/main/java/com/amazonaws/mobileconnectors/cognitoidentityprovider/CognitoUser.java index 7df4e901596..11fe2b8fc8e 100644 --- a/aws-android-sdk-cognitoidentityprovider/src/main/java/com/amazonaws/mobileconnectors/cognitoidentityprovider/CognitoUser.java +++ b/aws-android-sdk-cognitoidentityprovider/src/main/java/com/amazonaws/mobileconnectors/cognitoidentityprovider/CognitoUser.java @@ -45,6 +45,7 @@ import com.amazonaws.mobileconnectors.cognitoidentityprovider.tokens.CognitoIdToken; import com.amazonaws.mobileconnectors.cognitoidentityprovider.tokens.CognitoRefreshToken; import com.amazonaws.mobileconnectors.cognitoidentityprovider.util.CognitoDeviceHelper; +import com.amazonaws.mobileconnectors.cognitoidentityprovider.util.CognitoJWTParser; import com.amazonaws.mobileconnectors.cognitoidentityprovider.util.CognitoSecretHash; import com.amazonaws.mobileconnectors.cognitoidentityprovider.util.CognitoServiceConstants; import com.amazonaws.mobileconnectors.cognitoidentityprovider.util.Hkdf; @@ -83,6 +84,8 @@ import com.amazonaws.services.cognitoidentityprovider.model.ResourceNotFoundException; import com.amazonaws.services.cognitoidentityprovider.model.RespondToAuthChallengeRequest; import com.amazonaws.services.cognitoidentityprovider.model.RespondToAuthChallengeResult; +import com.amazonaws.services.cognitoidentityprovider.model.RevokeTokenRequest; +import com.amazonaws.services.cognitoidentityprovider.model.RevokeTokenResult; import com.amazonaws.services.cognitoidentityprovider.model.SMSMfaSettingsType; import com.amazonaws.services.cognitoidentityprovider.model.SetUserMFAPreferenceRequest; import com.amazonaws.services.cognitoidentityprovider.model.SetUserMFAPreferenceResult; @@ -2332,6 +2335,28 @@ private void deleteAttributesInternal(final List attributeNamesToDelete, cognitoIdentityProviderClient.deleteUserAttributes(deleteUserAttributesRequest); } + public RevokeTokenResult revokeTokens() { + try { + CognitoUserSession cognitoUserSession = getCachedSession(); + String accessToken = cognitoUserSession.getAccessToken().getJWTToken(); + if (CognitoJWTParser.hasClaim(accessToken, "origin_jti")) { + String refreshToken = cognitoUserSession.getRefreshToken().getToken(); + RevokeTokenRequest request = new RevokeTokenRequest(); + request.setToken(refreshToken); + request.setClientId(clientId); + if (!StringUtils.isBlank(clientSecret)) { + request.setClientSecret(clientSecret); + } + return cognitoIdentityProviderClient.revokeToken(request); + } else { + LOGGER.debug("Access Token does not contain `origin_jti` claim. Skip revoking tokens."); + } + } catch (final Exception e) { + LOGGER.warn("Failed to revoke tokens.", e); + } + return null; + } + /** * Sign-Out this user by removing all cached tokens. */ diff --git a/aws-android-sdk-cognitoidentityprovider/src/main/java/com/amazonaws/mobileconnectors/cognitoidentityprovider/util/CognitoJWTParser.java b/aws-android-sdk-cognitoidentityprovider/src/main/java/com/amazonaws/mobileconnectors/cognitoidentityprovider/util/CognitoJWTParser.java index d7cd326464d..53eccaacad0 100644 --- a/aws-android-sdk-cognitoidentityprovider/src/main/java/com/amazonaws/mobileconnectors/cognitoidentityprovider/util/CognitoJWTParser.java +++ b/aws-android-sdk-cognitoidentityprovider/src/main/java/com/amazonaws/mobileconnectors/cognitoidentityprovider/util/CognitoJWTParser.java @@ -115,6 +115,20 @@ public static String getClaim(String jwt, String claim) { return null; } + /** + * Checks if a JWT token contains a claim. + * @param jwt A string, possibly not event a JWT + * @param key Key for a claim, e.g., "jti" or "aud" + * @return True if JWT is a valid JWT and contains the requested claim, false otherwise + */ + public static boolean hasClaim(String jwt, String key) { + try { + return getPayload(jwt).has(key); + } catch (Exception e) { + return false; + } + } + /** * Checks if {@code JWT} is a valid JSON Web Token. * diff --git a/aws-android-sdk-mobile-client/src/androidTest/java/com/amazonaws/mobile/client/AWSMobileClientTest.java b/aws-android-sdk-mobile-client/src/androidTest/java/com/amazonaws/mobile/client/AWSMobileClientTest.java index 41754597dca..7a7f0425673 100644 --- a/aws-android-sdk-mobile-client/src/androidTest/java/com/amazonaws/mobile/client/AWSMobileClientTest.java +++ b/aws-android-sdk-mobile-client/src/androidTest/java/com/amazonaws/mobile/client/AWSMobileClientTest.java @@ -31,6 +31,16 @@ import com.amazonaws.mobile.client.results.Tokens; import com.amazonaws.mobile.client.results.UserCodeDeliveryDetails; import com.amazonaws.mobile.config.AWSConfiguration; +import com.amazonaws.mobileconnectors.cognitoidentityprovider.CognitoDevice; +import com.amazonaws.mobileconnectors.cognitoidentityprovider.CognitoUser; +import com.amazonaws.mobileconnectors.cognitoidentityprovider.CognitoUserDetails; +import com.amazonaws.mobileconnectors.cognitoidentityprovider.CognitoUserPool; +import com.amazonaws.mobileconnectors.cognitoidentityprovider.CognitoUserSession; +import com.amazonaws.mobileconnectors.cognitoidentityprovider.continuations.AuthenticationContinuation; +import com.amazonaws.mobileconnectors.cognitoidentityprovider.continuations.ChallengeContinuation; +import com.amazonaws.mobileconnectors.cognitoidentityprovider.continuations.MultiFactorAuthenticationContinuation; +import com.amazonaws.mobileconnectors.cognitoidentityprovider.handlers.AuthenticationHandler; +import com.amazonaws.mobileconnectors.cognitoidentityprovider.handlers.GetDetailsHandler; import com.amazonaws.regions.Region; import com.amazonaws.regions.Regions; import com.amazonaws.services.cognitoidentity.AmazonCognitoIdentity; @@ -50,6 +60,7 @@ import com.amazonaws.services.cognitoidentityprovider.model.ListUsersRequest; import com.amazonaws.services.cognitoidentityprovider.model.ListUsersResult; import com.amazonaws.services.cognitoidentityprovider.model.MessageActionType; +import com.amazonaws.services.cognitoidentityprovider.model.NotAuthorizedException; import com.amazonaws.services.cognitoidentityprovider.model.ResourceNotFoundException; import com.amazonaws.services.cognitoidentityprovider.model.UserNotConfirmedException; import com.amazonaws.services.cognitoidentityprovider.model.UserType; @@ -104,6 +115,7 @@ public class AWSMobileClientTest extends AWSMobileClientTestBase { private static final int THROTTLED_DELAY = 5000; static AmazonCognitoIdentityProvider userpoolLL; + static CognitoUserPool userPool; static { try { @@ -117,6 +129,8 @@ public class AWSMobileClientTest extends AWSMobileClientTestBase { static Regions clientRegion = Regions.US_WEST_2; static String userPoolId; static String identityPoolId; + static String clientId; + static String clientSecret; Context appContext; AWSMobileClient auth; @@ -226,6 +240,8 @@ public void onError(Exception e) { assertNotNull(userPoolConfig); clientRegion = Regions.fromName(userPoolConfig.getString("Region")); userPoolId = userPoolConfig.getString("PoolId"); + clientId = userPoolConfig.getString("AppClientId"); + clientSecret = userPoolConfig.optString("AppClientSecret"); JSONObject identityPoolConfig = awsConfiguration.optJsonObject("CredentialsProvider").getJSONObject( @@ -233,6 +249,8 @@ public void onError(Exception e) { assertNotNull(identityPoolConfig); identityPoolId = identityPoolConfig.getString("PoolId"); + userPool = new CognitoUserPool(appContext, userPoolId, clientId, clientSecret, clientRegion); + deleteAllUsers(userPoolId); createUserViaAdminAPI(userPoolId, USERNAME_ADMIN_API_USER, EMAIL_ADMIN_API_USER); } @@ -413,6 +431,124 @@ public void onUserStateChanged(UserStateDetails details) { assertNotEquals(getPackageConfigure().getString("identity_id"), details.toString()); } + @Test + public void testRevokeTokenWithSignedInUser() throws Exception { + auth.signIn(username, PASSWORD, null); + assertTrue("isSignedIn is true", auth.isSignedIn()); + + final AtomicReference tokenRevoked = new AtomicReference(false); + final CountDownLatch revokeTokenLatch = new CountDownLatch(2); + final CognitoUser user = userPool.getCurrentUser(); + user.getSession(new AuthenticationHandler() { + @Override + public void onSuccess(CognitoUserSession userSession, CognitoDevice newDevice) { + revokeTokenLatch.countDown(); + } + + @Override + public void getAuthenticationDetails(AuthenticationContinuation authenticationContinuation, String userId) { + + } + + @Override + public void getMFACode(MultiFactorAuthenticationContinuation continuation) { + + } + + @Override + public void authenticationChallenge(ChallengeContinuation continuation) { + + } + + @Override + public void onFailure(Exception exception) { + exception.printStackTrace(); + fail("Sign in failed."); + } + }); + + user.getDetails(new GetDetailsHandler() { + @Override + public void onSuccess(CognitoUserDetails cognitoUserDetails) { + revokeTokenLatch.countDown(); + } + + @Override + public void onFailure(Exception exception) { + exception.printStackTrace(); + fail("Get user details failed."); + } + }); + + try { + user.revokeTokens(); + tokenRevoked.set(true); + } catch (Exception e) { + e.printStackTrace(); + } + + revokeTokenLatch.await(5, TimeUnit.SECONDS); + assertTrue(tokenRevoked.get()); + + user.getDetails(new GetDetailsHandler() { + @Override + public void onSuccess(CognitoUserDetails cognitoUserDetails) { + fail("Request to get user details should fail with NotAuthorizedException after token is revoked."); + } + + @Override + public void onFailure(Exception exception) { + assertTrue(exception instanceof NotAuthorizedException); + } + }); + } + + @Test + public void testRevokeTokenWithSignedOutUser() throws Exception { + auth.signIn(username, PASSWORD, null); + assertTrue("isSignedIn is true", auth.isSignedIn()); + + final CountDownLatch revokeTokenLatch = new CountDownLatch(1); + final CognitoUser user = userPool.getCurrentUser(); + user.getSession(new AuthenticationHandler() { + @Override + public void onSuccess(CognitoUserSession userSession, CognitoDevice newDevice) { + revokeTokenLatch.countDown(); + } + + @Override + public void getAuthenticationDetails(AuthenticationContinuation authenticationContinuation, String userId) { + + } + + @Override + public void getMFACode(MultiFactorAuthenticationContinuation continuation) { + + } + + @Override + public void authenticationChallenge(ChallengeContinuation continuation) { + + } + + @Override + public void onFailure(Exception exception) { + exception.printStackTrace(); + fail("Sign in failed."); + } + }); + revokeTokenLatch.await(5, TimeUnit.SECONDS); + + auth.signOut(); + assertFalse("isSignedIn is false", auth.isSignedIn()); + + try { + user.revokeTokens(); + } catch (Exception e) { + assertTrue(e instanceof InvalidParameterException); + } + } + @Test public void testIdentityId() throws Exception { try { @@ -524,6 +660,28 @@ public void testSignOut() throws Exception { } } + @Test + public void testSignedOutWithRevokeToken() throws Exception { + auth.signIn(username, PASSWORD, null); + assertTrue("isSignedIn is true", auth.isSignedIn()); + + String tokenWithOriginJTI = "eyJraWQiOiIwTmxhQUhzbmtwQW5zbHBzUFhHWkJKcVJoR3E5WTkwckwweXpaWUV1OTJZPSIsImFsZyI6IlJTMjU2In0.eyJvcmlnaW5fanRpIjoiMzM2MWFkZDMtMDIwNS00NTY1LTk0MjQtMDQ3YWQ2N2Y0MjhmZWwifQ.a"; + setAccessToken(appContext, clientId, username, tokenWithOriginJTI); + auth.signOut(); + assertFalse("isSignedIn is false", auth.isSignedIn()); + } + + @Test + public void testSignedOutWithoutRevokeToken() throws Exception { + auth.signIn(username, PASSWORD, null); + assertTrue("isSignedIn is true", auth.isSignedIn()); + + String tokenWithSub = "eyJraWQiOiJzU01EYmZyQ21pb3FrbEVRZFprNXl6UmszekxSTlo4aGlGMnlxdVFZbVM0PSIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiI3YTQyNTFmMS04MmEyLTQxNzgtOWZhOS1mNmE3MTc1RCJ9.a"; + setAccessToken(appContext, clientId, username, tokenWithSub); + auth.signOut(); + assertFalse("isSignedIn is false", auth.isSignedIn()); + } + @Test(expected = com.amazonaws.services.cognitoidentityprovider.model.NotAuthorizedException.class) public void testSignInWrongPassword() throws Exception { AWSMobileClient.getInstance().signIn(getPackageConfigure().getString("username"), "wrong", null); diff --git a/aws-android-sdk-mobile-client/src/androidTest/java/com/amazonaws/mobile/client/AWSMobileClientTestBase.java b/aws-android-sdk-mobile-client/src/androidTest/java/com/amazonaws/mobile/client/AWSMobileClientTestBase.java index 5002bc1c986..3f8f2f0528b 100644 --- a/aws-android-sdk-mobile-client/src/androidTest/java/com/amazonaws/mobile/client/AWSMobileClientTestBase.java +++ b/aws-android-sdk-mobile-client/src/androidTest/java/com/amazonaws/mobile/client/AWSMobileClientTestBase.java @@ -71,6 +71,14 @@ public static void writeUserPoolsTokens(final Context appContext, final String c awsKeyValueStore.put(storeFieldPrefix + "refreshToken", "DummyRefresh"); } + public static void setAccessToken(final Context appContext, final String clientId, final String username, final String accessToken) { + final AWSKeyValueStore awsKeyValueStore = new AWSKeyValueStore(appContext, + "CognitoIdentityProviderCache", + true); + String storeFieldPrefix = "CognitoIdentityProvider." + clientId + "." + username + "."; + awsKeyValueStore.put(storeFieldPrefix + "accessToken", accessToken); + } + public static void writeUserPoolsTokens(final Context appContext, final String clientId, final String userId, diff --git a/aws-android-sdk-mobile-client/src/androidTest/java/com/amazonaws/mobile/client/AbstractAmazonCognitoIdentity.java b/aws-android-sdk-mobile-client/src/androidTest/java/com/amazonaws/mobile/client/AbstractAmazonCognitoIdentity.java index d1ab7e279bb..51e46449b5c 100644 --- a/aws-android-sdk-mobile-client/src/androidTest/java/com/amazonaws/mobile/client/AbstractAmazonCognitoIdentity.java +++ b/aws-android-sdk-mobile-client/src/androidTest/java/com/amazonaws/mobile/client/AbstractAmazonCognitoIdentity.java @@ -42,6 +42,8 @@ import com.amazonaws.services.cognitoidentity.model.GetOpenIdTokenForDeveloperIdentityResult; import com.amazonaws.services.cognitoidentity.model.GetOpenIdTokenRequest; import com.amazonaws.services.cognitoidentity.model.GetOpenIdTokenResult; +import com.amazonaws.services.cognitoidentity.model.GetPrincipalTagAttributeMapRequest; +import com.amazonaws.services.cognitoidentity.model.GetPrincipalTagAttributeMapResult; import com.amazonaws.services.cognitoidentity.model.ListIdentitiesRequest; import com.amazonaws.services.cognitoidentity.model.ListIdentitiesResult; import com.amazonaws.services.cognitoidentity.model.ListIdentityPoolsRequest; @@ -53,6 +55,8 @@ import com.amazonaws.services.cognitoidentity.model.MergeDeveloperIdentitiesRequest; import com.amazonaws.services.cognitoidentity.model.MergeDeveloperIdentitiesResult; import com.amazonaws.services.cognitoidentity.model.SetIdentityPoolRolesRequest; +import com.amazonaws.services.cognitoidentity.model.SetPrincipalTagAttributeMapRequest; +import com.amazonaws.services.cognitoidentity.model.SetPrincipalTagAttributeMapResult; import com.amazonaws.services.cognitoidentity.model.TagResourceRequest; import com.amazonaws.services.cognitoidentity.model.TagResourceResult; import com.amazonaws.services.cognitoidentity.model.UnlinkDeveloperIdentityRequest; @@ -123,6 +127,11 @@ public GetOpenIdTokenForDeveloperIdentityResult getOpenIdTokenForDeveloperIdenti return null; } + @Override + public GetPrincipalTagAttributeMapResult getPrincipalTagAttributeMap(GetPrincipalTagAttributeMapRequest getPrincipalTagAttributeMapRequest) throws AmazonClientException, AmazonServiceException { + return null; + } + @Override public ListIdentitiesResult listIdentities(ListIdentitiesRequest listIdentitiesRequest) throws AmazonClientException, AmazonServiceException { return null; @@ -153,6 +162,11 @@ public void setIdentityPoolRoles(SetIdentityPoolRolesRequest setIdentityPoolRole } + @Override + public SetPrincipalTagAttributeMapResult setPrincipalTagAttributeMap(SetPrincipalTagAttributeMapRequest setPrincipalTagAttributeMapRequest) throws AmazonClientException, AmazonServiceException { + return null; + } + @Override public TagResourceResult tagResource(TagResourceRequest tagResourceRequest) throws AmazonClientException, AmazonServiceException { return null; diff --git a/aws-android-sdk-mobile-client/src/androidTest/java/com/amazonaws/mobile/client/AbstractAmazonCognitoIdentityProvider.java b/aws-android-sdk-mobile-client/src/androidTest/java/com/amazonaws/mobile/client/AbstractAmazonCognitoIdentityProvider.java index ef3f385d0f2..3f2dff10aff 100644 --- a/aws-android-sdk-mobile-client/src/androidTest/java/com/amazonaws/mobile/client/AbstractAmazonCognitoIdentityProvider.java +++ b/aws-android-sdk-mobile-client/src/androidTest/java/com/amazonaws/mobile/client/AbstractAmazonCognitoIdentityProvider.java @@ -434,6 +434,11 @@ public RespondToAuthChallengeResult respondToAuthChallenge(RespondToAuthChalleng return null; } + @Override + public RevokeTokenResult revokeToken(RevokeTokenRequest revokeTokenRequest) throws AmazonClientException, AmazonServiceException { + return null; + } + @Override public SetRiskConfigurationResult setRiskConfiguration(SetRiskConfigurationRequest setRiskConfigurationRequest) throws AmazonClientException, AmazonServiceException { return null; diff --git a/aws-android-sdk-mobile-client/src/main/java/com/amazonaws/mobile/client/AWSMobileClient.java b/aws-android-sdk-mobile-client/src/main/java/com/amazonaws/mobile/client/AWSMobileClient.java index dda961d0f0d..dc8329f0c2d 100644 --- a/aws-android-sdk-mobile-client/src/main/java/com/amazonaws/mobile/client/AWSMobileClient.java +++ b/aws-android-sdk-mobile-client/src/main/java/com/amazonaws/mobile/client/AWSMobileClient.java @@ -1611,6 +1611,9 @@ public Void run() throws Exception { userpoolLL.globalSignOut(globalSignOutRequest); } if (signOutOptions.isInvalidateTokens()) { + if (userpool != null) { + userpool.getCurrentUser().revokeTokens(); + } if (hostedUI != null) { if (signOutOptions.getBrowserPackage() != null) { hostedUI.setBrowserPackage(signOutOptions.getBrowserPackage());