Skip to content

Commit 68f10bc

Browse files
Fixed retrigger of completeUserLogin on new auth token
1 parent a576cb8 commit 68f10bc

File tree

5 files changed

+110
-30
lines changed

5 files changed

+110
-30
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
All notable changes to this project will be documented in this file.
33
This project adheres to [Semantic Versioning](http://semver.org/).
44

5+
## [Unreleased]
6+
7+
### Added
8+
- Added `updateAuthToken(String)` method for updating the auth token without triggering login side effects (push registration, in-app sync, embedded sync). Use this when you only need to refresh the token for an already logged-in user.
9+
10+
### Deprecated
11+
- `setAuthToken(String)` is now deprecated. It still triggers login operations (push registration, in-app sync, embedded sync) for backward compatibility, but will be changed to only store the token in a future release. Migrate to `updateAuthToken(String)` to update the token without side effects, or use `setEmail(email, authToken)` / `setUserId(userId, authToken)` to set credentials and trigger login operations.
12+
513
## [3.6.5]
614
### Fixed
715
- Fixed IterableEmbeddedView not having an empty constructor and causing crashes

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

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ public String getAuthToken() {
137137
private void checkAndUpdateAuthToken(@Nullable String authToken) {
138138
// If authHandler exists and if authToken is new, it will be considered as a call to update the authToken.
139139
if (config.authHandler != null && authToken != null && authToken != _authToken) {
140-
setAuthToken(authToken);
140+
updateAuthToken(authToken);
141141
}
142142
}
143143

@@ -425,20 +425,24 @@ private void onLogin(
425425
@Nullable IterableHelper.FailureHandler failureHandler
426426
) {
427427
if (!isInitialized()) {
428-
setAuthToken(null);
428+
updateAuthToken(null);
429429
return;
430430
}
431431

432432
getAuthManager().pauseAuthRetries(false);
433433
if (authToken != null) {
434-
setAuthToken(authToken);
434+
updateAuthToken(authToken);
435+
completeUserLogin();
435436
attemptMergeAndEventReplay(userIdOrEmail, isEmail, merge, replay, isUnknown, failureHandler);
436437
} else {
437-
getAuthManager().requestNewAuthToken(false, data -> attemptMergeAndEventReplay(userIdOrEmail, isEmail, merge, replay, isUnknown, failureHandler));
438+
getAuthManager().requestNewAuthToken(false, data -> {
439+
completeUserLogin();
440+
attemptMergeAndEventReplay(userIdOrEmail, isEmail, merge, replay, isUnknown, failureHandler);
441+
});
438442
}
439443
}
440444

441-
private void completeUserLogin() {
445+
void completeUserLogin() {
442446
completeUserLogin(_email, _userId, _authToken);
443447
}
444448

@@ -679,19 +683,16 @@ public void resetAuth() {
679683

680684
//region API functions (private/internal)
681685
//---------------------------------------------------------------------------------------
682-
void setAuthToken(String authToken, boolean bypassAuth) {
686+
687+
/**
688+
* Updates the auth token without triggering login side effects (push registration, in-app sync, etc.).
689+
* Use this method when you only need to update the token for an already logged-in user.
690+
* For initial login, use {@code setEmail(email, authToken)} or {@code setUserId(userId, authToken)}.
691+
*/
692+
public void updateAuthToken(@Nullable String authToken) {
683693
if (isInitialized()) {
684-
if ((authToken != null && !authToken.equalsIgnoreCase(_authToken)) || (_authToken != null && !_authToken.equalsIgnoreCase(authToken))) {
685-
_authToken = authToken;
686-
// SECURITY: Use completion handler to atomically store and pass validated credentials.
687-
// The completion handler receives exact values stored to keychain, preventing TOCTOU
688-
// attacks where keychain could be modified between storage and completeUserLogin execution.
689-
storeAuthData((email, userId, token) -> completeUserLogin(email, userId, token));
690-
} else if (bypassAuth) {
691-
// SECURITY: Pass current credentials directly to completeUserLogin.
692-
// completeUserLogin will validate authToken presence when JWT auth is enabled.
693-
completeUserLogin(_email, _userId, _authToken);
694-
}
694+
_authToken = authToken;
695+
storeAuthData();
695696
}
696697
}
697698

@@ -1211,8 +1212,24 @@ private void attemptAndProcessMerge(@NonNull String destinationUser, boolean isE
12111212
});
12121213
}
12131214

1214-
public void setAuthToken(String authToken) {
1215-
setAuthToken(authToken, false);
1215+
/**
1216+
* Sets the auth token and triggers login operations (push registration, in-app sync, embedded sync).
1217+
*
1218+
* @deprecated This method triggers login side effects beyond just setting the token.
1219+
* To update the auth token without login side effects, use {@link #updateAuthToken(String)}.
1220+
* To set credentials and trigger login operations, use {@code setEmail(email, authToken)}
1221+
* or {@code setUserId(userId, authToken)}.
1222+
* In a future release, this method will only store the auth token without triggering login operations.
1223+
*/
1224+
@Deprecated
1225+
public void setAuthToken(@Nullable String authToken) {
1226+
if (isInitialized()) {
1227+
IterableLogger.w(TAG, "setAuthToken() is deprecated. Use updateAuthToken() to update the token, " +
1228+
"or setEmail(email, authToken) / setUserId(userId, authToken) for login. " +
1229+
"In a future release, this method will only store the auth token without triggering login operations.");
1230+
_authToken = authToken;
1231+
storeAuthData(this::completeUserLogin);
1232+
}
12161233
}
12171234

12181235
/**

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ public void run() {
204204
}
205205

206206
} else {
207-
IterableApi.getInstance().setAuthToken(null, true);
207+
IterableApi.getInstance().completeUserLogin();
208208
}
209209
}
210210

@@ -213,15 +213,15 @@ private void handleAuthTokenSuccess(String authToken, IterableHelper.SuccessHand
213213
// Token obtained but not yet verified by a request - set state to UNKNOWN.
214214
// setAuthState will notify listeners only if previous state was INVALID.
215215
setAuthState(AuthState.UNKNOWN);
216-
IterableApi.getInstance().setAuthToken(authToken);
216+
IterableApi.getInstance().updateAuthToken(authToken);
217217
queueExpirationRefresh(authToken);
218218

219219
if (successCallback != null) {
220220
handleSuccessForAuthToken(authToken, successCallback);
221221
}
222222
} else {
223223
handleAuthFailure(authToken, AuthFailureReason.AUTH_TOKEN_NULL);
224-
IterableApi.getInstance().setAuthToken(authToken);
224+
IterableApi.getInstance().updateAuthToken(authToken);
225225
scheduleAuthTokenRefresh(getNextRetryInterval(), false, null);
226226
return;
227227
}

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,13 @@ public void testCompleteUserLogin_WithJWTAuth_NoToken_SkipsSensitiveOps() throws
9999
when(api.getInAppManager()).thenReturn(mockInAppManager);
100100
when(api.getEmbeddedManager()).thenReturn(mockEmbeddedManager);
101101

102-
// Directly call setAuthToken with null and bypassAuth=true to simulate
102+
// Directly call updateAuthToken with null to simulate
103103
// attempting to bypass with no token (user-controlled bypass scenario)
104-
api.setAuthToken(null, true);
104+
api.updateAuthToken(null);
105105

106106
shadowOf(getMainLooper()).idle();
107107

108-
// Verify sensitive operations were NOT called (JWT auth enabled, no token)
108+
// Verify sensitive operations were NOT called (updateAuthToken only stores, no login side effects)
109109
verify(mockInAppManager, never()).syncInApp();
110110
verify(mockEmbeddedManager, never()).syncMessages();
111111
}
@@ -246,14 +246,16 @@ public void testSetAuthToken_UsesCompletionHandlerPattern() throws Exception {
246246
org.mockito.Mockito.clearInvocations(mockInAppManager, mockEmbeddedManager);
247247

248248
// Now update auth token (simulating token refresh)
249+
// updateAuthToken just stores the token — it does not trigger completeUserLogin.
250+
// Sensitive operations (syncInApp, syncMessages) are only triggered during login flow.
249251
final String newToken = "new_jwt_token_here";
250-
api.setAuthToken(newToken, false);
252+
api.updateAuthToken(newToken);
251253

252254
shadowOf(getMainLooper()).idle();
253255

254-
// Verify sensitive operations were called with updated token
255-
verify(mockInAppManager).syncInApp();
256-
verify(mockEmbeddedManager).syncMessages();
256+
// Verify sensitive operations were NOT called (updateAuthToken only stores, doesn't trigger login)
257+
verify(mockInAppManager, never()).syncInApp();
258+
verify(mockEmbeddedManager, never()).syncMessages();
257259
assertEquals("Token should be updated", newToken, api.getAuthToken());
258260
}
259261

@@ -274,7 +276,7 @@ public void testSetAuthToken_BypassAuth_StillValidatesToken() throws Exception {
274276
when(api.getEmbeddedManager()).thenReturn(mockEmbeddedManager);
275277

276278
// Try to bypass with no token set
277-
api.setAuthToken(null, true);
279+
api.updateAuthToken(null);
278280

279281
shadowOf(getMainLooper()).idle();
280282

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,14 @@
2323
import static junit.framework.Assert.assertEquals;
2424
import static junit.framework.Assert.assertNotNull;
2525
import static junit.framework.Assert.assertNull;
26+
import static org.mockito.ArgumentMatchers.any;
2627
import static org.mockito.Mockito.doReturn;
2728
import static org.mockito.Mockito.mock;
29+
import static org.mockito.Mockito.never;
30+
import static org.mockito.Mockito.verify;
2831
import static org.robolectric.Shadows.shadowOf;
32+
33+
import org.mockito.Mockito;
2934
import static org.robolectric.annotation.LooperMode.Mode.PAUSED;
3035

3136
@LooperMode(PAUSED)
@@ -509,4 +514,52 @@ public void testAuthTokenRefreshPausesOnBackground() throws Exception {
509514
// Test passes if no exceptions were thrown and lifecycle methods executed successfully
510515
}
511516

517+
@Test
518+
public void testTokenRefreshDoesNotTriggerPushRegistration() throws Exception {
519+
IterablePushRegistration.IterablePushRegistrationImpl originalPushImpl = IterablePushRegistration.instance;
520+
IterablePushRegistration.instance = mock(IterablePushRegistration.IterablePushRegistrationImpl.class);
521+
522+
try {
523+
// Initialize with auth and auto push registration enabled
524+
IterableApi.sharedInstance = new IterableApi();
525+
authHandler = mock(IterableAuthHandler.class);
526+
IterableApi.initialize(getContext(), "apiKey",
527+
new IterableConfig.Builder()
528+
.setAuthHandler(authHandler)
529+
.setAutoPushRegistration(true)
530+
.setPushIntegrationName("pushIntegration")
531+
.build());
532+
533+
// Initial login: setEmail triggers requestNewAuthToken on executor thread
534+
doReturn(validJWT).when(authHandler).onAuthTokenRequested();
535+
IterableApi.getInstance().setEmail("test@example.com");
536+
// Allow executor thread to complete and main looper to process callbacks
537+
Thread.sleep(500);
538+
shadowOf(getMainLooper()).idle();
539+
shadowOf(getMainLooper()).runToEndOfTasks();
540+
541+
// Verify initial push registration happened
542+
verify(IterablePushRegistration.instance).executePushRegistrationTask(any(IterablePushRegistrationData.class));
543+
544+
// Reset mock to clear invocation history
545+
Mockito.reset(IterablePushRegistration.instance);
546+
547+
// Trigger auth token refresh with a different JWT
548+
doReturn(newJWT).when(authHandler).onAuthTokenRequested();
549+
IterableApi.getInstance().getAuthManager().requestNewAuthToken(false, null);
550+
// Allow executor thread to complete and main looper to process callbacks
551+
Thread.sleep(500);
552+
shadowOf(getMainLooper()).idle();
553+
shadowOf(getMainLooper()).runToEndOfTasks();
554+
555+
// Verify the token was actually refreshed
556+
assertEquals(newJWT, IterableApi.getInstance().getAuthToken());
557+
558+
// Assert that push registration was NOT called again on token refresh
559+
verify(IterablePushRegistration.instance, never()).executePushRegistrationTask(any(IterablePushRegistrationData.class));
560+
} finally {
561+
IterablePushRegistration.instance = originalPushImpl;
562+
}
563+
}
564+
512565
}

0 commit comments

Comments
 (0)