@@ -420,10 +420,34 @@ private void onLogin(
420420 }
421421
422422 private void completeUserLogin () {
423+ completeUserLogin (_email , _userId , _authToken );
424+ }
425+
426+ /**
427+ * Completes user login with validated credentials.
428+ * This method ensures sensitive operations (syncInApp, syncMessages, registerForPush) only execute
429+ * with server-validated user data, preventing user-controlled bypass attacks.
430+ *
431+ * @param email Server-validated email (can be null)
432+ * @param userId Server-validated userId (can be null)
433+ * @param authToken Server-validated authToken (must not be null for sensitive operations when JWT auth is enabled)
434+ */
435+ private void completeUserLogin (@ Nullable String email , @ Nullable String userId , @ Nullable String authToken ) {
423436 if (!isInitialized ()) {
424437 return ;
425438 }
426439
440+ // Only enforce authToken requirement when JWT auth is enabled
441+ // This prevents user-controlled bypass where unvalidated userId/email from keychain
442+ // could be used to access another user's data in JWT auth scenarios
443+ if (config .authHandler != null && authToken == null ) {
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 );
447+ }
448+ return ;
449+ }
450+
427451 if (config .autoPushRegistration ) {
428452 registerForPush ();
429453 } else if (_setUserSuccessCallbackHandler != null ) {
@@ -502,10 +526,45 @@ private String getDeviceId() {
502526 return _deviceId ;
503527 }
504528
529+ /**
530+ * Completion handler interface for storeAuthData operations.
531+ * Receives the exact credentials that were stored to keychain.
532+ */
533+ private interface AuthDataStorageHandler {
534+ void onAuthDataStored (String email , String userId , String authToken );
535+ }
536+
505537 private void storeAuthData () {
538+ storeAuthData (null );
539+ }
540+
541+ /**
542+ * Stores auth data and optionally invokes completion handler with the stored credentials.
543+ *
544+ * SECURITY - TOCTOU Protection:
545+ * When a completion handler is provided, it receives the exact credentials that were stored
546+ * to keychain. This prevents TOCTOU (Time-Of-Check-Time-Of-Use) attacks where:
547+ * 1. Credentials are stored to keychain
548+ * 2. Attacker modifies keychain (between store and read)
549+ * 3. Sensitive operations use tampered credentials
550+ *
551+ * By capturing credentials BEFORE storage and passing them directly via completion handler,
552+ * we ensure completeUserLogin uses exactly what was stored, not what's currently in keychain.
553+ *
554+ * @param completionHandler Optional handler invoked synchronously after storage with the stored credentials
555+ */
556+ private void storeAuthData (AuthDataStorageHandler completionHandler ) {
506557 if (_applicationContext == null ) {
507558 return ;
508559 }
560+
561+ // SECURITY: Capture current instance field values BEFORE storing to keychain.
562+ // These captured values will be passed to completion handler, ensuring the caller
563+ // receives exactly what was stored, not potentially modified keychain data.
564+ final String storedEmail = _email ;
565+ final String storedUserId = _userId ;
566+ final String storedAuthToken = _authToken ;
567+
509568 IterableKeychain iterableKeychain = getKeychain ();
510569 if (iterableKeychain != null ) {
511570 iterableKeychain .saveEmail (_email );
@@ -515,6 +574,11 @@ private void storeAuthData() {
515574 } else {
516575 IterableLogger .e (TAG , "Shared preference creation failed. " );
517576 }
577+
578+ // Invoke completion handler with the captured credentials
579+ if (completionHandler != null ) {
580+ completionHandler .onAuthDataStored (storedEmail , storedUserId , storedAuthToken );
581+ }
518582 }
519583
520584 private void retrieveEmailAndUserId () {
@@ -595,10 +659,14 @@ void setAuthToken(String authToken, boolean bypassAuth) {
595659 if (isInitialized ()) {
596660 if ((authToken != null && !authToken .equalsIgnoreCase (_authToken )) || (_authToken != null && !_authToken .equalsIgnoreCase (authToken ))) {
597661 _authToken = authToken ;
598- storeAuthData ();
599- completeUserLogin ();
662+ // SECURITY: Use completion handler to atomically store and pass validated credentials.
663+ // The completion handler receives exact values stored to keychain, preventing TOCTOU
664+ // attacks where keychain could be modified between storage and completeUserLogin execution.
665+ storeAuthData ((email , userId , token ) -> completeUserLogin (email , userId , token ));
600666 } else if (bypassAuth ) {
601- completeUserLogin ();
667+ // SECURITY: Pass current credentials directly to completeUserLogin.
668+ // completeUserLogin will validate authToken presence when JWT auth is enabled.
669+ completeUserLogin (_email , _userId , _authToken );
602670 }
603671 }
604672 }
0 commit comments