Skip to content

Commit 16e95c7

Browse files
authored
fix(mobileclient): Honor auth flow setting from config (#2499)
* fix(mobileclient): Honor auth flow setting from config * PR feedback
1 parent 1ebe614 commit 16e95c7

File tree

3 files changed

+268
-8
lines changed

3 files changed

+268
-8
lines changed

aws-android-sdk-mobile-client/build.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ dependencies {
4949
compileOnly project(':aws-android-sdk-cognitoauth')
5050
compileOnly 'androidx.browser:browser:1.2.0'
5151

52+
testImplementation 'junit:junit:4.13.2'
53+
testImplementation 'org.mockito:mockito-core:3.2.4'
54+
testImplementation 'org.robolectric:robolectric:4.4'
55+
testImplementation 'androidx.test:core:1.3.0'
56+
testImplementation project(':aws-android-sdk-cognitoauth')
57+
5258
androidTestImplementation 'androidx.appcompat:appcompat:1.2.0'
5359
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
5460
androidTestImplementation 'androidx.test:rules:1.3.0'

aws-android-sdk-mobile-client/src/main/java/com/amazonaws/mobile/client/AWSMobileClient.java

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import android.os.Build;
3030
import android.util.Log;
3131
import androidx.annotation.AnyThread;
32+
import androidx.annotation.VisibleForTesting;
3233
import androidx.annotation.WorkerThread;
3334
import androidx.browser.customtabs.CustomTabsCallback;
3435
import androidx.browser.customtabs.CustomTabsClient;
@@ -129,6 +130,9 @@
129130
import java.util.concurrent.locks.ReentrantLock;
130131

131132
import static com.amazonaws.mobile.client.results.SignInState.CUSTOM_CHALLENGE;
133+
import static com.amazonaws.mobileconnectors.cognitoidentityprovider.util.CognitoServiceConstants.AUTH_TYPE_INIT_CUSTOM_AUTH;
134+
import static com.amazonaws.mobileconnectors.cognitoidentityprovider.util.CognitoServiceConstants.AUTH_TYPE_INIT_USER_PASSWORD;
135+
import static com.amazonaws.mobileconnectors.cognitoidentityprovider.util.CognitoServiceConstants.CHLG_TYPE_USER_PASSWORD;
132136

133137
/**
134138
* The AWSMobileClient provides client APIs and building blocks for developers who want to create
@@ -346,6 +350,25 @@ public static synchronized AWSMobileClient getInstance() {
346350
return singleton;
347351
}
348352

353+
/**
354+
* For unit testing purposes only so we can force a new instance of
355+
* mobile client to be created.
356+
* @param forceGetNew True if a new instance of AWSMobileClient should be created.
357+
* @return A new instance of AWSMobileClient.
358+
*/
359+
@VisibleForTesting
360+
static synchronized AWSMobileClient getInstance(boolean forceGetNew) {
361+
if (forceGetNew) {
362+
singleton = null;
363+
}
364+
return new AWSMobileClient();
365+
}
366+
367+
@VisibleForTesting
368+
void setUserPool(CognitoUserPool userpool) {
369+
this.userpool = userpool;
370+
}
371+
349372
/**
350373
* Retrieve the AWSConfiguration object that represents
351374
* the awsconfiguration.json file.
@@ -1213,23 +1236,40 @@ public void onSuccess(CognitoUserSession userSession, CognitoDevice newDevice) {
12131236
@Override
12141237
public void getAuthenticationDetails(AuthenticationContinuation authenticationContinuation, String userId) {
12151238
Log.d(TAG, "Sending password.");
1239+
final HashMap<String, String> authParameters = new HashMap<>();
1240+
// Check if the auth flow type setting is in the configuration.
1241+
boolean authFlowTypeInConfig =
1242+
awsConfiguration.optJsonObject(AUTH_KEY) != null &&
1243+
awsConfiguration.optJsonObject(AUTH_KEY).has("authenticationFlowType");
1244+
12161245
try {
1217-
if (
1218-
awsConfiguration.optJsonObject(AUTH_KEY) != null &&
1219-
awsConfiguration.optJsonObject(AUTH_KEY).has("authenticationFlowType") &&
1220-
awsConfiguration.optJsonObject(AUTH_KEY).getString("authenticationFlowType").equals("CUSTOM_AUTH")
1221-
) {
1222-
final HashMap<String, String> authParameters = new HashMap<String, String>();
1246+
String authFlowType = authFlowTypeInConfig ?
1247+
awsConfiguration.optJsonObject(AUTH_KEY).getString("authenticationFlowType") :
1248+
null;
1249+
if (authFlowTypeInConfig && AUTH_TYPE_INIT_CUSTOM_AUTH.equals(authFlowType)) {
1250+
// If there's a value in the config and it's CUSTOM_AUTH, we'll
1251+
// use one of the below constructors depending on what's passed in.
12231252
if (password != null) {
12241253
authenticationContinuation.setAuthenticationDetails(new AuthenticationDetails(username, password, authParameters, validationData));
12251254
} else {
12261255
authenticationContinuation.setAuthenticationDetails(new AuthenticationDetails(username, authParameters, validationData));
12271256
}
1257+
} else if (authFlowTypeInConfig && AUTH_TYPE_INIT_USER_PASSWORD.equals(authFlowType)) {
1258+
// If there's a value in the config and it's USER_PASSWORD_AUTH, set the auth type (challenge name)
1259+
// to be USER_PASSWORD.
1260+
AuthenticationDetails authenticationDetails = new AuthenticationDetails(username, password, validationData);
1261+
authenticationDetails.setAuthenticationType(CHLG_TYPE_USER_PASSWORD);
1262+
authenticationContinuation.setAuthenticationDetails(authenticationDetails);
1263+
12281264
} else {
1265+
// Otherwise, auth flow is USER_SRP_AUTH and the auth type (challenge name)
1266+
// will default to PASSWORD_VERIFIER.
1267+
Log.d(TAG, "Using USER_SRP_AUTH for flow type.");
12291268
authenticationContinuation.setAuthenticationDetails(new AuthenticationDetails(username, password, validationData));
12301269
}
1231-
} catch (JSONException e) {
1232-
e.printStackTrace();
1270+
1271+
} catch (JSONException exception) {
1272+
Log.w(TAG, "Exception while attempting to read authenticationFlowType from config.", exception);
12331273
}
12341274

12351275
authenticationContinuation.continueTask();
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
/*
2+
* Copyright 2021 Amazon.com, Inc. or its affiliates.
3+
* All Rights Reserved.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package com.amazonaws.mobile.client;
19+
20+
import android.util.Log;
21+
22+
import com.amazonaws.mobile.client.results.SignInResult;
23+
import com.amazonaws.mobile.config.AWSConfiguration;
24+
import com.amazonaws.mobileconnectors.cognitoidentityprovider.CognitoUser;
25+
import com.amazonaws.mobileconnectors.cognitoidentityprovider.CognitoUserPool;
26+
import com.amazonaws.mobileconnectors.cognitoidentityprovider.continuations.AuthenticationContinuation;
27+
import com.amazonaws.mobileconnectors.cognitoidentityprovider.continuations.AuthenticationDetails;
28+
import com.amazonaws.mobileconnectors.cognitoidentityprovider.handlers.AuthenticationHandler;
29+
30+
import org.json.JSONException;
31+
import org.json.JSONObject;
32+
import org.junit.Before;
33+
import org.junit.Test;
34+
import org.junit.runner.RunWith;
35+
import org.mockito.ArgumentCaptor;
36+
import org.robolectric.RobolectricTestRunner;
37+
import org.robolectric.shadows.ShadowLog;
38+
39+
import java.util.Collections;
40+
import java.util.concurrent.CountDownLatch;
41+
import java.util.concurrent.TimeUnit;
42+
import java.util.concurrent.atomic.AtomicBoolean;
43+
44+
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
45+
import static org.junit.Assert.assertEquals;
46+
import static org.junit.Assert.assertTrue;
47+
import static org.mockito.ArgumentMatchers.any;
48+
import static org.mockito.ArgumentMatchers.anyString;
49+
import static org.mockito.Mockito.doAnswer;
50+
import static org.mockito.Mockito.mock;
51+
import static org.mockito.Mockito.verify;
52+
import static org.mockito.Mockito.when;
53+
54+
@RunWith(RobolectricTestRunner.class)
55+
public class AWSMobileClientAuthFlowSettingTest {
56+
private static final String TAG = AWSMobileClientAuthFlowSettingTest.class.getSimpleName();
57+
private CountDownLatch initLatch;
58+
private CountDownLatch signinLatch;
59+
private CountDownLatch continuationLatch;
60+
private AtomicBoolean initComplete;
61+
private AtomicBoolean signInComplete;
62+
private AtomicBoolean continuationComplete;
63+
private AWSMobileClient mobileClient;
64+
65+
@Before
66+
public void beforeEachTest() {
67+
ShadowLog.stream = System.out;
68+
initLatch = new CountDownLatch(1);
69+
signinLatch = new CountDownLatch(1);
70+
continuationLatch = new CountDownLatch(1);
71+
initComplete = new AtomicBoolean(false);
72+
signInComplete = new AtomicBoolean(false);
73+
continuationComplete = new AtomicBoolean(false);
74+
try {
75+
mobileClient = AWSMobileClient.getInstance(true);
76+
} catch (Exception exception) {
77+
exception.printStackTrace();
78+
}
79+
}
80+
81+
@Test
82+
public void testUserPasswordAuth() throws JSONException, InterruptedException {
83+
String flowFromConfig = "USER_PASSWORD_AUTH";
84+
String expectedAuthType = "USER_PASSWORD";
85+
verifyScenario(flowFromConfig, expectedAuthType);
86+
}
87+
88+
@Test
89+
public void testUserSrpAuth() throws JSONException, InterruptedException {
90+
String flowFromConfig = "USER_SRP_AUTH";
91+
String expectedAuthType = "PASSWORD_VERIFIER";
92+
verifyScenario(flowFromConfig, expectedAuthType);
93+
}
94+
95+
@Test
96+
public void testCustomAuth() throws JSONException, InterruptedException {
97+
String flowFromConfig = "CUSTOM_AUTH";
98+
String expectedAuthType = "CUSTOM_CHALLENGE";
99+
verifyScenario(flowFromConfig, expectedAuthType);
100+
}
101+
102+
@Test
103+
public void testNoAuthFlowInConfig() throws JSONException, InterruptedException {
104+
String flowFromConfig = null;
105+
String expectedAuthType = "PASSWORD_VERIFIER";
106+
verifyScenario(flowFromConfig, expectedAuthType);
107+
}
108+
109+
/**
110+
* Verify that the correct auth type (aka challenge name) is passed in based on the
111+
* auth flow type from the config file.
112+
* @param authFlowType The auth flow type from the config.
113+
* @param expectedAuthType The auth type expected.
114+
* @throws JSONException Not expected.
115+
* @throws InterruptedException Not expected.
116+
*/
117+
private void verifyScenario(String authFlowType, String expectedAuthType) throws JSONException, InterruptedException {
118+
AuthenticationContinuation mockContinuation = setupMockContinuation();
119+
CognitoUserPool mockUserPool = setupMockUserPool(mockContinuation);
120+
initMobileClientAndWait(authFlowType, mockUserPool);
121+
signinAndWait();
122+
ArgumentCaptor<AuthenticationDetails> argumentCaptor = ArgumentCaptor.forClass(AuthenticationDetails.class);
123+
verify(mockContinuation).setAuthenticationDetails(argumentCaptor.capture());
124+
AuthenticationDetails actualAuthDetails = argumentCaptor.getValue();
125+
assertEquals(expectedAuthType, actualAuthDetails.getAuthenticationType());
126+
}
127+
128+
private void signinAndWait() throws InterruptedException {
129+
mobileClient.signIn("test",
130+
"fakepassword",
131+
Collections.emptyMap(),
132+
Collections.emptyMap(),
133+
new Callback<SignInResult>() {
134+
@Override
135+
public void onResult(SignInResult result) {
136+
signInComplete.set(true);
137+
signinLatch.countDown();
138+
}
139+
140+
@Override
141+
public void onError(Exception e) {
142+
e.printStackTrace();
143+
signinLatch.countDown();
144+
}
145+
});
146+
147+
signinLatch.await(5, TimeUnit.SECONDS);
148+
continuationLatch.await(5, TimeUnit.SECONDS);
149+
}
150+
151+
private void initMobileClientAndWait(String authFlowType, CognitoUserPool mockUserPool) throws JSONException, InterruptedException {
152+
JSONObject awsConfig = getAWSConfig(authFlowType);
153+
mobileClient.initialize(getApplicationContext(), new AWSConfiguration(awsConfig), new Callback<UserStateDetails>() {
154+
@Override
155+
public void onResult(UserStateDetails result) {
156+
mobileClient.setUserPool(mockUserPool);
157+
Log.i(TAG, "Mobile client initialized. Mock user pool injected.");
158+
initComplete.set(true);
159+
initLatch.countDown();
160+
}
161+
162+
@Override
163+
public void onError(Exception exception) {
164+
Log.e(TAG, "Mobile client failed to initialize.", exception);
165+
initLatch.countDown();
166+
}
167+
});
168+
initLatch.await(5, TimeUnit.SECONDS);
169+
assertTrue(initComplete.get());
170+
}
171+
172+
private AuthenticationContinuation setupMockContinuation() {
173+
AuthenticationContinuation mockContinuation = mock(AuthenticationContinuation.class);
174+
175+
doAnswer(invocation -> {
176+
Log.i(TAG, "Counting down continuation latch.");
177+
continuationLatch.countDown();
178+
continuationComplete.set(true);
179+
return null;
180+
}).when(mockContinuation).continueTask();
181+
return mockContinuation;
182+
}
183+
184+
private CognitoUserPool setupMockUserPool(AuthenticationContinuation mockContinuation) {
185+
CognitoUserPool mockUserPool = mock(CognitoUserPool.class);
186+
CognitoUser mockUser = mock(CognitoUser.class);
187+
188+
doAnswer(invocation -> {
189+
int indexOfHandler = 1;
190+
AuthenticationHandler handler = invocation.getArgument(indexOfHandler, AuthenticationHandler.class);
191+
handler.getAuthenticationDetails(mockContinuation, "FAKE_USER_ID");
192+
return null;
193+
}).when(mockUser).getSession(any(), any());
194+
when(mockUserPool.getUser(anyString())).thenReturn(mockUser);
195+
return mockUserPool;
196+
}
197+
198+
private JSONObject getAWSConfig(String expectedAuthFlow) throws JSONException {
199+
return new JSONObject()
200+
.put("Auth", new JSONObject()
201+
.put("Default", new JSONObject()
202+
.put("authenticationFlowType", expectedAuthFlow)
203+
.put("Persistence", false)
204+
)
205+
)
206+
.put("CognitoUserPool", new JSONObject()
207+
.put("Default", new JSONObject()
208+
.put("PoolId", "TEST_POOL_ID")
209+
.put("Region", "us-east-1")
210+
.put("AppClientId", "TEST_CLIENT_ID")
211+
)
212+
);
213+
}
214+
}

0 commit comments

Comments
 (0)