/* * Copyright 2019 Amazon.com, Inc. or its affiliates. * All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.amazonaws.mobile.client; import android.content.Context; import android.util.Log; import androidx.test.core.app.ApplicationProvider; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.internal.keyvaluestore.AWSKeyValueStore; import com.amazonaws.mobile.client.results.SignInResult; import com.amazonaws.mobile.client.results.SignInState; import com.amazonaws.mobile.client.results.Token; import com.amazonaws.mobile.client.results.Tokens; import com.amazonaws.mobile.config.AWSConfiguration; import com.amazonaws.mobileconnectors.cognitoauth.AuthUserSession; import com.amazonaws.mobileconnectors.cognitoauth.tokens.AccessToken; import com.amazonaws.mobileconnectors.cognitoauth.tokens.IdToken; import com.amazonaws.mobileconnectors.cognitoauth.tokens.RefreshToken; import com.amazonaws.mobileconnectors.cognitoauth.util.LocalDataManager; import com.amazonaws.regions.Region; import com.amazonaws.regions.Regions; import com.amazonaws.services.cognitoidentityprovider.AmazonCognitoIdentityProvider; import com.amazonaws.services.cognitoidentityprovider.AmazonCognitoIdentityProviderClient; import com.amazonaws.services.cognitoidentityprovider.model.AdminConfirmSignUpRequest; import com.amazonaws.services.cognitoidentityprovider.model.AdminDeleteUserRequest; import com.amazonaws.services.cognitoidentityprovider.model.ListUsersRequest; import com.amazonaws.services.cognitoidentityprovider.model.ListUsersResult; import com.amazonaws.services.cognitoidentityprovider.model.UserNotConfirmedException; import com.amazonaws.services.cognitoidentityprovider.model.UserType; import org.json.JSONException; import org.json.JSONObject; import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import static com.amazonaws.mobile.client.AWSMobileClient.*; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public class AWSMobileClientPersistenceWithRestartabilityTest extends AWSMobileClientTestBase { private static final String TAG = AWSMobileClientPersistenceTest.class.getSimpleName(); private static final String EMAIL = "success+user@simulator.amazonses.com"; private static final String USERNAME = "somebody"; private static final String PASSWORD = "1234Password!"; private static AmazonCognitoIdentityProvider cognitoUserPoolLowLevelClient; // Populated from awsconfiguration.json private static Regions clientRegion = Regions.US_WEST_2; private static String userPoolId; private Context appContext; private AWSMobileClient auth; private UserStateListener listener; private String username; public static synchronized AmazonCognitoIdentityProvider getCognitoUserPoolLowLevelClient() { if (cognitoUserPoolLowLevelClient == null) { cognitoUserPoolLowLevelClient = new AmazonCognitoIdentityProviderClient(credentials); cognitoUserPoolLowLevelClient.setRegion(Region.getRegion(clientRegion)); } return cognitoUserPoolLowLevelClient; } public static void createUser(final AWSMobileClient auth, final String userPoolId, final String username, final String password, final String email) throws Exception { HashMap<String, String> userAttributes = new HashMap<String, String>(); userAttributes.put("email", email); auth.signUp(username, password, userAttributes,null); AdminConfirmSignUpRequest adminConfirmSignUpRequest = new AdminConfirmSignUpRequest(); adminConfirmSignUpRequest.withUsername(username).withUserPoolId(userPoolId); getCognitoUserPoolLowLevelClient().adminConfirmSignUp(adminConfirmSignUpRequest); } public static void deleteAllUsers(final String userPoolId) { ListUsersResult listUsersResult; do { ListUsersRequest listUsersRequest = new ListUsersRequest() .withUserPoolId(userPoolId) .withLimit(60); listUsersResult = getCognitoUserPoolLowLevelClient().listUsers(listUsersRequest); for (UserType user : listUsersResult.getUsers()) { if (USERNAME.equals(user.getUsername()) || "bimin".equals(user.getUsername())) { // This user is saved to test the identity id permanence continue; } boolean retryConfirmSignUp = false; do { try { Log.d(TAG, "deleteAllUsers: " + user.getUsername()); getCognitoUserPoolLowLevelClient().adminDeleteUser(new AdminDeleteUserRequest().withUsername(user.getUsername()).withUserPoolId(userPoolId)); } catch (UserNotConfirmedException e) { if (!retryConfirmSignUp) { AdminConfirmSignUpRequest adminConfirmSignUpRequest = new AdminConfirmSignUpRequest(); adminConfirmSignUpRequest.withUsername(user.getUsername()).withUserPoolId(userPoolId); getCognitoUserPoolLowLevelClient().adminConfirmSignUp(adminConfirmSignUpRequest); retryConfirmSignUp = true; try { Thread.sleep(10); } catch (InterruptedException e1) { e1.printStackTrace(); } } else { retryConfirmSignUp = false; } } catch (Exception e) { Log.e(TAG, "deleteAllUsers: Some error trying to delete user", e); } } while (retryConfirmSignUp); } } while (listUsersResult.getPaginationToken() != null); } @BeforeClass public static void beforeClass() throws Exception { setUpCredentials(); Context appContext = ApplicationProvider.getApplicationContext(); final CountDownLatch latch = new CountDownLatch(1); AWSMobileClient.getInstance().initialize(appContext, new Callback<UserStateDetails>() { @Override public void onResult(UserStateDetails result) { latch.countDown(); } @Override public void onError(Exception e) { latch.countDown(); } }); latch.await(); final AWSConfiguration awsConfiguration = AWSMobileClient.getInstance().getConfiguration(); JSONObject userPoolConfig = awsConfiguration.optJsonObject("CognitoUserPool"); assertNotNull(userPoolConfig); clientRegion = Regions.fromName(userPoolConfig.getString("Region")); userPoolId = userPoolConfig.getString("PoolId"); JSONObject identityPoolConfig = awsConfiguration.optJsonObject("CredentialsProvider").getJSONObject( "CognitoIdentity").getJSONObject("Default"); assertNotNull(identityPoolConfig); deleteAllUsers(userPoolId); } @Before public void before() throws Exception { appContext = ApplicationProvider.getApplicationContext(); auth = AWSMobileClient.getInstance(); auth.signOut(); username = "testUser" + System.currentTimeMillis() + new Random().nextInt(); createUser(auth, userPoolId, username, PASSWORD, EMAIL); } @After public void after() { auth.removeUserStateListener(listener); auth.listeners.clear(); auth.signOut(); auth.mStore.clear(); appContext.getSharedPreferences(AWSMobileClient.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) .edit() .clear() .apply(); deleteAllEncryptionKeys(); } // When the encryption keys are lost and retrieval of // tokens from persistent store fails, calling getTokens() // will throw an exception and the UserStateListener will // be triggered the userState as SIGNED_OUT. Followed by a // SignIn operation, getTokens() will retrieve // the tokens successfully and trigger the userState as // SIGNED_IN. @Test public void testGetTokens() throws Exception { signInAndVerifySignIn(); final CountDownLatch stateNotificationLatch = new CountDownLatch(1); final AtomicReference<UserStateDetails> userState = new AtomicReference<UserStateDetails>(); listener = new UserStateListener() { @Override public void onUserStateChanged(UserStateDetails details) { userState.set(details); stateNotificationLatch.countDown(); } }; auth.addUserStateListener(listener); deleteAllEncryptionKeys(); initializeAWSMobileClient(appContext, UserState.SIGNED_OUT); try { auth.getTokens(); } catch (Exception ex) { assertEquals("getTokens does not support retrieving tokens while signed-out", ex.getMessage()); } stateNotificationLatch.await(10, TimeUnit.SECONDS); assertEquals(UserState.SIGNED_OUT, userState.get().getUserState()); try { final SignInResult signInResult = auth.signIn(username, PASSWORD, null); assertNotNull(signInResult); assertEquals(SignInState.DONE, signInResult.getSignInState()); } catch (Exception ex) { fail(ex.getMessage()); } Tokens tokens = auth.getTokens(); verifyTokens(tokens); assertEquals(UserState.SIGNED_IN, userState.get().getUserState()); } @Test public void testGetUserName() { signInAndVerifySignIn(); assertNotNull(auth.getUsername()); deleteAllEncryptionKeys(); assertNull(auth.getUsername()); try { final SignInResult signInResult = auth.signIn(username, PASSWORD, null); assertNotNull(signInResult); assertEquals(SignInState.DONE, signInResult.getSignInState()); } catch (Exception ex) { fail(ex.getMessage()); } assertNotNull(auth.getUsername()); } @Test public void testGetUserAttributes() throws Exception { signInAndVerifySignIn(); Map<String, String> userAttributes = auth.getUserAttributes(); assertNotNull(userAttributes); assertEquals(getPackageConfigure().getString("email"), userAttributes.get("email")); deleteAllEncryptionKeys(); initializeAWSMobileClient(appContext, UserState.SIGNED_OUT); try { auth.getUserAttributes(); } catch (Exception ex) { assertEquals("Operation requires a signed-in state", ex.getMessage()); } try { final SignInResult signInResult = auth.signIn(username, PASSWORD, null); assertNotNull(signInResult); assertEquals(SignInState.DONE, signInResult.getSignInState()); } catch (Exception ex) { fail(ex.getMessage()); } userAttributes = auth.getUserAttributes(); assertNotNull(userAttributes); assertEquals(getPackageConfigure().getString("email"), userAttributes.get("email")); } @Test public void testIsSignedIn() { signInAndVerifySignIn(); assertTrue(auth.isSignedIn()); deleteAllEncryptionKeys(); initializeAWSMobileClient(appContext, UserState.SIGNED_OUT); assertFalse(auth.isSignedIn()); try { final SignInResult signInResult = auth.signIn(username, PASSWORD, null); assertNotNull(signInResult); assertEquals(SignInState.DONE, signInResult.getSignInState()); } catch (Exception ex) { fail(ex.getMessage()); } assertTrue(auth.isSignedIn()); } @Test public void testGetCredentials() { signInAndVerifySignIn(); AWSCredentials awsCredentialsBeforeEncryptionKeysAreLost = auth.getCredentials(); assertNotNull(awsCredentialsBeforeEncryptionKeysAreLost); deleteAllEncryptionKeys(); initializeAWSMobileClient(appContext, UserState.SIGNED_OUT); AWSCredentials awsCredentialsAfterEncryptionKeysAreLost = auth.getCredentials(); assertNotNull(awsCredentialsAfterEncryptionKeysAreLost); assertEquals(awsCredentialsBeforeEncryptionKeysAreLost.getAWSAccessKeyId(), awsCredentialsAfterEncryptionKeysAreLost.getAWSAccessKeyId()); assertEquals(awsCredentialsBeforeEncryptionKeysAreLost.getAWSSecretKey(), awsCredentialsAfterEncryptionKeysAreLost.getAWSSecretKey()); } @Test public void testGetIdentityId() { signInAndVerifySignIn(); String identityId = auth.getIdentityId(); assertNotNull(identityId); deleteAllEncryptionKeys(); initializeAWSMobileClient(appContext, UserState.SIGNED_OUT); assertNull(auth.getIdentityId()); try { final SignInResult signInResult = auth.signIn(username, PASSWORD, null); assertNotNull(signInResult); assertEquals(SignInState.DONE, signInResult.getSignInState()); } catch (Exception ex) { fail(ex.getMessage()); } String identityIdAfterSecondSignIn = auth.getIdentityId(); assertNotNull(identityIdAfterSecondSignIn); assertEquals(identityId, identityIdAfterSecondSignIn); } // -- Hosted UI based tests @Test public void testHostedUIObjectNotNullAfterInitialize() { auth = initializeAWSMobileClient(appContext, UserState.SIGNED_OUT); assertNotNull(auth.hostedUI); } @Test public void testHostedUIObjectNotNullAfterSignOut() { auth = initializeAWSMobileClient(appContext, UserState.SIGNED_OUT); auth.signOut(); assertNotNull(auth.hostedUI); } @Test public void testHostedUIObjectNotNullAfterAppRestarted() { auth = initializeAWSMobileClient(appContext, UserState.SIGNED_OUT); auth.signOut(); mockRestartingApp(UserState.SIGNED_OUT); assertNotNull(auth.hostedUI); } @Test public void testHostedUIGetTokens() throws Exception { auth = initializeAWSMobileClient(appContext, UserState.SIGNED_OUT); mockHostedUISignIn(); Tokens tokens = auth.getTokens(false); assertNotNull(tokens); } @Test public void testHostedUIGetTokensAfterAppRestarted() throws Exception { auth = initializeAWSMobileClient(appContext, UserState.SIGNED_OUT); mockHostedUISignIn(); mockRestartingApp(UserState.SIGNED_IN); Tokens tokens = auth.getTokens(false); assertNotNull(tokens); } private void signInAndVerifySignIn() { try { final CountDownLatch stateNotificationLatch = new CountDownLatch(1); final AtomicReference<UserStateDetails> userState = new AtomicReference<UserStateDetails>(); listener = new UserStateListener() { @Override public void onUserStateChanged(UserStateDetails details) { userState.set(details); auth.removeUserStateListener(listener); stateNotificationLatch.countDown(); } }; auth.addUserStateListener(listener); final SignInResult signInResult = auth.signIn(username, PASSWORD, null); assertEquals("Cannot support MFA in tests", SignInState.DONE, signInResult.getSignInState()); assertTrue("isSignedIn is true", auth.isSignedIn()); assertEquals(username, auth.getUsername()); // Check credentials are available final AWSCredentials credentials = auth.getCredentials(); assertNotNull("Credentials are null", credentials); assertNotNull("Access key is null", credentials.getAWSAccessKeyId()); assertNotNull("Secret key is null", credentials.getAWSSecretKey()); Tokens tokens = auth.getTokens(); assertNotNull(tokens); Token accessToken = tokens.getAccessToken(); assertNotNull(accessToken); assertTrue("Access token should not be expired", accessToken.getExpiration().after(new Date())); Token idToken = tokens.getIdToken(); assertNotNull(idToken); assertTrue("Id token should not be expired", idToken.getExpiration().after(new Date())); Token refreshToken = tokens.getRefreshToken(); assertNotNull(refreshToken); // Check one attribute final Map<String, String> userAttributes = auth.getUserAttributes(); assertEquals(getPackageConfigure().getString("email"), userAttributes.get("email")); stateNotificationLatch.await(5, TimeUnit.SECONDS); UserStateDetails userStateDetails = userState.get(); assertEquals(userStateDetails.getUserState(), UserState.SIGNED_IN); Map<String, String> details = userStateDetails.getDetails(); assertNotEquals(getPackageConfigure().getString("identity_id"), details.toString()); } catch (Exception ex) { fail(ex.getMessage()); } } // Note that most tests create valid JWT tokens with expiry dates in the past. However, because // we want to assert that HostedUI can get tokens, without making a network call to refresh a // session, we're going to mock up valid session data, and ensure we call `getTokens` with // `waitForSignIn = false`. private void mockHostedUISignIn() throws JSONException { AuthUserSession authUserSession = new AuthUserSession( new IdToken(getValidJWT(3600L)), new AccessToken(getValidJWT(3600L)), new RefreshToken(getValidJWT(360000L))); Context targetContext = ApplicationProvider.getApplicationContext(); AWSKeyValueStore storeForHostedUI = new AWSKeyValueStore( targetContext, "CognitoIdentityProviderCache", true); final Set<String> scopes = new HashSet<String>(Arrays.asList("aws.cognito.signin.user.admin", "phone", "openid", "profile", "email")); LocalDataManager.cacheSession(storeForHostedUI, targetContext, getPackageConfigure().getString("app_client_id"), getPackageConfigure().getString("username"), authUserSession, scopes); // Set the AWSMobileClient metadata that is specific to HostedUI auth.mStore.set(FEDERATION_ENABLED_KEY, "true"); auth.mStore.set(HOSTED_UI_KEY, "dummyJson"); auth.mStore.set(SIGN_IN_MODE, SignInMode.HOSTED_UI.toString()); auth.mStore.set(PROVIDER_KEY, auth.getLoginKey()); auth.mStore.set(TOKEN_KEY, getValidJWT(3600L)); } private void mockRestartingApp(UserState expectedUserState) { auth = null; auth = initializeAWSMobileClient(appContext, expectedUserState); } }