Skip to content

Commit 047c10c

Browse files
committed
Add tests
1 parent c84e94c commit 047c10c

File tree

2 files changed

+58
-62
lines changed

2 files changed

+58
-62
lines changed

iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -441,9 +441,9 @@ private void completeUserLogin(@Nullable String email, @Nullable String userId,
441441
// This prevents user-controlled bypass where unvalidated userId/email from keychain
442442
// could be used to access another user's data in JWT auth scenarios
443443
if (config.authHandler != null && authToken == null) {
444-
IterableLogger.d(TAG, "Skipping sensitive operations - JWT auth enabled but no validated authToken present");
445-
if (_setUserSuccessCallbackHandler != null) {
446-
_setUserSuccessCallbackHandler.onSuccess(new JSONObject());
444+
IterableLogger.w(TAG, "Cannot complete user login - JWT auth enabled but no validated authToken present");
445+
if (_setUserFailureCallbackHandler != null) {
446+
_setUserFailureCallbackHandler.onFailure("JWT authentication is enabled but no valid authToken is available", null);
447447
}
448448
return;
449449
}

iterableapi/src/test/java/com/iterable/iterableapi/IterableApiAuthSecurityTests.java

Lines changed: 55 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@
33
import static android.os.Looper.getMainLooper;
44
import static org.junit.Assert.assertEquals;
55
import static org.junit.Assert.assertNotNull;
6-
import static org.junit.Assert.assertNull;
7-
import static org.mockito.ArgumentMatchers.any;
86
import static org.mockito.ArgumentMatchers.anyString;
9-
import static org.mockito.ArgumentMatchers.eq;
107
import static org.mockito.Mockito.doAnswer;
118
import static org.mockito.Mockito.doReturn;
129
import static org.mockito.Mockito.mock;
@@ -25,7 +22,6 @@
2522
import org.junit.After;
2623
import org.junit.Before;
2724
import org.junit.Test;
28-
import org.mockito.ArgumentCaptor;
2925

3026
import java.io.IOException;
3127
import java.util.concurrent.CountDownLatch;
@@ -37,7 +33,7 @@
3733

3834
/**
3935
* Security tests for TOCTOU (Time-Of-Check-Time-Of-Use) vulnerabilities in auth flow.
40-
*
36+
*
4137
* These tests verify that credentials cannot be swapped mid-flight between storage
4238
* and usage, preventing user-controlled bypass attacks.
4339
*/
@@ -56,7 +52,7 @@ public void setUp() throws IOException {
5652
dispatcher = new PathBasedQueueDispatcher();
5753
server.setDispatcher(dispatcher);
5854
IterableApi.overrideURLEndpointPath(server.url("").toString());
59-
55+
6056
mockKeychain = mock(IterableKeychain.class);
6157
}
6258

@@ -93,22 +89,22 @@ private void initIterableWithoutAuth() {
9389
@Test
9490
public void testCompleteUserLogin_WithJWTAuth_NoToken_SkipsSensitiveOps() throws Exception {
9591
initIterableWithAuth();
96-
92+
9793
// Spy on the API instance to verify method calls
9894
IterableApi api = spy(IterableApi.getInstance());
9995
IterableApi.sharedInstance = api;
100-
96+
10197
IterableInAppManager mockInAppManager = mock(IterableInAppManager.class);
10298
IterableEmbeddedManager mockEmbeddedManager = mock(IterableEmbeddedManager.class);
10399
when(api.getInAppManager()).thenReturn(mockInAppManager);
104100
when(api.getEmbeddedManager()).thenReturn(mockEmbeddedManager);
105-
106-
// Directly call setAuthToken with null and bypassAuth=true to simulate
101+
102+
// Directly call setAuthToken with null and bypassAuth=true to simulate
107103
// attempting to bypass with no token (user-controlled bypass scenario)
108104
api.setAuthToken(null, true);
109-
105+
110106
shadowOf(getMainLooper()).idle();
111-
107+
112108
// Verify sensitive operations were NOT called (JWT auth enabled, no token)
113109
verify(mockInAppManager, never()).syncInApp();
114110
verify(mockEmbeddedManager, never()).syncMessages();
@@ -121,25 +117,25 @@ public void testCompleteUserLogin_WithJWTAuth_NoToken_SkipsSensitiveOps() throws
121117
@Test
122118
public void testCompleteUserLogin_WithJWTAuth_WithToken_ExecutesSensitiveOps() throws Exception {
123119
initIterableWithAuth();
124-
120+
125121
dispatcher.enqueueResponse("/users/update", new MockResponse().setResponseCode(200).setBody("{}"));
126-
122+
127123
doReturn(validJWT).when(authHandler).onAuthTokenRequested();
128-
124+
129125
// Spy on the API instance to verify method calls
130126
IterableApi api = spy(IterableApi.getInstance());
131127
IterableApi.sharedInstance = api;
132-
128+
133129
IterableInAppManager mockInAppManager = mock(IterableInAppManager.class);
134130
IterableEmbeddedManager mockEmbeddedManager = mock(IterableEmbeddedManager.class);
135131
when(api.getInAppManager()).thenReturn(mockInAppManager);
136132
when(api.getEmbeddedManager()).thenReturn(mockEmbeddedManager);
137-
133+
138134
api.setEmail("[email protected]");
139-
135+
140136
server.takeRequest(1, TimeUnit.SECONDS);
141137
shadowOf(getMainLooper()).idle();
142-
138+
143139
// Verify sensitive operations WERE called with valid token
144140
verify(mockInAppManager).syncInApp();
145141
verify(mockEmbeddedManager).syncMessages();
@@ -152,20 +148,20 @@ public void testCompleteUserLogin_WithJWTAuth_WithToken_ExecutesSensitiveOps() t
152148
@Test
153149
public void testCompleteUserLogin_WithoutJWTAuth_NoToken_ExecutesSensitiveOps() throws Exception {
154150
initIterableWithoutAuth();
155-
151+
156152
// Spy on the API instance to verify method calls
157153
IterableApi api = spy(IterableApi.getInstance());
158154
IterableApi.sharedInstance = api;
159-
155+
160156
IterableInAppManager mockInAppManager = mock(IterableInAppManager.class);
161157
IterableEmbeddedManager mockEmbeddedManager = mock(IterableEmbeddedManager.class);
162158
when(api.getInAppManager()).thenReturn(mockInAppManager);
163159
when(api.getEmbeddedManager()).thenReturn(mockEmbeddedManager);
164-
160+
165161
api.setEmail("[email protected]");
166-
162+
167163
shadowOf(getMainLooper()).idle();
168-
164+
169165
// Verify sensitive operations WERE called (no JWT auth required)
170166
verify(mockInAppManager).syncInApp();
171167
verify(mockEmbeddedManager).syncMessages();
@@ -178,13 +174,13 @@ public void testCompleteUserLogin_WithoutJWTAuth_NoToken_ExecutesSensitiveOps()
178174
@Test
179175
public void testStoreAuthData_CompletionHandler_ReceivesStoredCredentials() throws Exception {
180176
initIterableWithAuth();
181-
177+
182178
IterableApi api = IterableApi.getInstance();
183-
179+
184180
// Use reflection to spy on storeAuthData behavior
185181
IterableApi spyApi = spy(api);
186182
IterableApi.sharedInstance = spyApi;
187-
183+
188184
// Set up mock keychain that attempts to swap credentials mid-flight
189185
doAnswer(invocation -> {
190186
// Malicious keychain that tries to swap email after storage
@@ -193,17 +189,17 @@ public void testStoreAuthData_CompletionHandler_ReceivesStoredCredentials() thro
193189
// Simulate attacker trying to modify keychain after storage
194190
return null;
195191
}).when(mockKeychain).saveEmail(anyString());
196-
192+
197193
when(spyApi.getKeychain()).thenReturn(mockKeychain);
198-
194+
199195
dispatcher.enqueueResponse("/users/update", new MockResponse().setResponseCode(200).setBody("{}"));
200196
doReturn(validJWT).when(authHandler).onAuthTokenRequested();
201-
197+
202198
final String originalEmail = "[email protected]";
203199
final AtomicReference<String> completionHandlerEmail = new AtomicReference<>();
204200
final AtomicReference<String> completionHandlerToken = new AtomicReference<>();
205201
final CountDownLatch latch = new CountDownLatch(1);
206-
202+
207203
// Capture what the completion handler receives
208204
spyApi.setEmail(originalEmail, new IterableHelper.SuccessHandler() {
209205
@Override
@@ -212,11 +208,11 @@ public void onSuccess(JSONObject data) {
212208
latch.countDown();
213209
}
214210
}, null);
215-
211+
216212
server.takeRequest(1, TimeUnit.SECONDS);
217213
shadowOf(getMainLooper()).idle();
218214
latch.await(2, TimeUnit.SECONDS);
219-
215+
220216
// Verify the API instance has the correct email
221217
assertEquals("Email should match original", originalEmail, spyApi.getEmail());
222218
assertNotNull("AuthToken should be set", spyApi.getAuthToken());
@@ -229,32 +225,32 @@ public void onSuccess(JSONObject data) {
229225
@Test
230226
public void testSetAuthToken_UsesCompletionHandlerPattern() throws Exception {
231227
initIterableWithAuth();
232-
228+
233229
dispatcher.enqueueResponse("/users/update", new MockResponse().setResponseCode(200).setBody("{}"));
234230
doReturn(validJWT).when(authHandler).onAuthTokenRequested();
235-
231+
236232
IterableApi api = spy(IterableApi.getInstance());
237233
IterableApi.sharedInstance = api;
238-
234+
239235
IterableInAppManager mockInAppManager = mock(IterableInAppManager.class);
240236
IterableEmbeddedManager mockEmbeddedManager = mock(IterableEmbeddedManager.class);
241237
when(api.getInAppManager()).thenReturn(mockInAppManager);
242238
when(api.getEmbeddedManager()).thenReturn(mockEmbeddedManager);
243-
239+
244240
// First set user
245241
api.setEmail("[email protected]");
246242
server.takeRequest(1, TimeUnit.SECONDS);
247243
shadowOf(getMainLooper()).idle();
248-
244+
249245
// Clear previous invocations
250246
org.mockito.Mockito.clearInvocations(mockInAppManager, mockEmbeddedManager);
251-
247+
252248
// Now update auth token (simulating token refresh)
253249
final String newToken = "new_jwt_token_here";
254250
api.setAuthToken(newToken, false);
255-
251+
256252
shadowOf(getMainLooper()).idle();
257-
253+
258254
// Verify sensitive operations were called with updated token
259255
verify(mockInAppManager).syncInApp();
260256
verify(mockEmbeddedManager).syncMessages();
@@ -268,20 +264,20 @@ public void testSetAuthToken_UsesCompletionHandlerPattern() throws Exception {
268264
@Test
269265
public void testSetAuthToken_BypassAuth_StillValidatesToken() throws Exception {
270266
initIterableWithAuth();
271-
267+
272268
IterableApi api = spy(IterableApi.getInstance());
273269
IterableApi.sharedInstance = api;
274-
270+
275271
IterableInAppManager mockInAppManager = mock(IterableInAppManager.class);
276272
IterableEmbeddedManager mockEmbeddedManager = mock(IterableEmbeddedManager.class);
277273
when(api.getInAppManager()).thenReturn(mockInAppManager);
278274
when(api.getEmbeddedManager()).thenReturn(mockEmbeddedManager);
279-
275+
280276
// Try to bypass with no token set
281277
api.setAuthToken(null, true);
282-
278+
283279
shadowOf(getMainLooper()).idle();
284-
280+
285281
// Verify sensitive operations were NOT called (JWT auth enabled, no token)
286282
verify(mockInAppManager, never()).syncInApp();
287283
verify(mockEmbeddedManager, never()).syncMessages();
@@ -294,32 +290,32 @@ public void testSetAuthToken_BypassAuth_StillValidatesToken() throws Exception {
294290
@Test
295291
public void testCredentialConsistency_StorageToUsage() throws Exception {
296292
initIterableWithAuth();
297-
293+
298294
dispatcher.enqueueResponse("/users/update", new MockResponse().setResponseCode(200).setBody("{}"));
299-
295+
300296
final String testEmail = "[email protected]";
301297
doReturn(validJWT).when(authHandler).onAuthTokenRequested();
302-
298+
303299
IterableApi api = IterableApi.getInstance();
304-
300+
305301
// Set email
306302
api.setEmail(testEmail);
307303
server.takeRequest(1, TimeUnit.SECONDS);
308304
shadowOf(getMainLooper()).idle();
309-
305+
310306
// Verify credentials match exactly
311307
assertEquals("Email should match", testEmail, api.getEmail());
312308
assertEquals("AuthToken should match", validJWT, api.getAuthToken());
313-
309+
314310
// Now test with userId
315311
dispatcher.enqueueResponse("/users/update", new MockResponse().setResponseCode(200).setBody("{}"));
316312
final String testUserId = "user123";
317313
doReturn(validJWT).when(authHandler).onAuthTokenRequested();
318-
314+
319315
api.setUserId(testUserId);
320316
server.takeRequest(1, TimeUnit.SECONDS);
321317
shadowOf(getMainLooper()).idle();
322-
318+
323319
// Verify userId matches
324320
assertEquals("UserId should match", testUserId, api.getUserId());
325321
assertEquals("AuthToken should still match", validJWT, api.getAuthToken());
@@ -339,20 +335,20 @@ public void testStaleKeychainCredentials_NoToken_SkipsSensitiveOps() throws Exce
339335
editor.putString(IterableConstants.SHARED_PREFS_USERID_KEY, "staleUserId");
340336
// No auth token stored
341337
editor.apply();
342-
338+
343339
initIterableWithAuth();
344-
340+
345341
IterableApi api = spy(IterableApi.getInstance());
346342
IterableApi.sharedInstance = api;
347-
343+
348344
IterableInAppManager mockInAppManager = mock(IterableInAppManager.class);
349345
IterableEmbeddedManager mockEmbeddedManager = mock(IterableEmbeddedManager.class);
350346
when(api.getInAppManager()).thenReturn(mockInAppManager);
351347
when(api.getEmbeddedManager()).thenReturn(mockEmbeddedManager);
352-
348+
353349
// Trigger initialization flow
354350
shadowOf(getMainLooper()).idle();
355-
351+
356352
// Verify sensitive operations were NOT called with stale credentials
357353
verify(mockInAppManager, never()).syncInApp();
358354
verify(mockEmbeddedManager, never()).syncMessages();

0 commit comments

Comments
 (0)