Skip to content

fix(jwt): Multi-user fixes for identity verification - WIP for manual testing#2586

Open
nan-li wants to merge 12 commits intoidentity_verification_betafrom
jwt_fixes_handle_multi_user
Open

fix(jwt): Multi-user fixes for identity verification - WIP for manual testing#2586
nan-li wants to merge 12 commits intoidentity_verification_betafrom
jwt_fixes_handle_multi_user

Conversation

@nan-li
Copy link
Contributor

@nan-li nan-li commented Mar 19, 2026

Description

One Line Summary

Fix JWT identity verification to support multi-user operation queues, per-operation JWT tracking, and correct per-user API routing.

Details

Motivation

When identity verification (JWT) is enabled, multiple bugs cause an infinite 401 retry loop:

  1. Operations for previous users use the current user's JWT and identity alias in API calls, causing mismatched 401s
  2. FAIL_UNAUTHORIZED re-enqueues operations indefinitely with no retry limit
  3. A single invalid JWT blocks the entire queue, preventing other users' operations from executing
  4. UserJwtInvalidatedListener fires with the current user's externalId instead of the failing operation's user

Scope

  • Per-operation JWT storage: Each operation now carries its own operationJwt and operationExternalId, stamped at enqueue time. This makes operations self-contained for multi-user support.
  • Skip-blocked, don't block-all: getNextOps() skips operations with null JWT (waiting for refresh) rather than blocking the entire queue. Other users' operations continue executing.
  • FAIL_UNAUTHORIZED handling: Nulls JWT per-operation, adds unauthorizedRetries counter (max 3), fires UserJwtInvalidatedListener with the correct operationExternalId.
  • updateJwtForExternalId(): New method on OperationRepo updates JWT on all queued operations matching an externalId, resets retry counters, and wakes the queue.
  • Operation-derived identity alias: Executors now derive the API URL path alias (external_id vs onesignal_id) from the operation and useIdentityVerification config -- not from the current user's IdentityModelStore.
  • Anonymous user handling: When IV is enabled, operations without an externalId (requiresJwt=true) are discarded. UpdateSubscriptionOperation has requiresJwt=false so it always executes (needed for logout).
  • Remote params gate: getNextOps() blocks until isInitializedWithRemote is true, preventing operations from executing before IV status is known.
  • Listener wiring moved: UserJwtInvalidatedListener now fires from OperationRepo (with correct per-operation externalId) instead of UserManager (which used current user's externalId).
  • Demo app: Updated "Login User" and "Update JWT" buttons to accept two inputs (externalId + JWT) for easier testing.

Not changed: logout() behavior, login() enqueue logic, operation grouping semantics, LoginUserOperationExecutor create-user flow.

Testing

Unit testing

  • 18 new/updated tests in OperationRepoTests.kt covering: skip-blocked logic, anonymous op discarding, isInitializedWithRemote gate, FAIL_UNAUTHORIZED retry limits, updateJwtForExternalId, multi-user execution, enqueue stamping, follow-up op inheritance
  • 8 new tests across 4 executor test files (Identity, RefreshUser, Subscription, UpdateUser) verifying operation-derived identity alias for both IV-enabled (external_id) and IV-disabled (onesignal_id) paths

Manual testing

  • Tested with two users (nan00, nan01) with invalid JWTs, verified operations queue independently
  • Updated nan00's JWT, verified nan00's operations execute with correct identity while nan01's remain skipped
  • Verified UserJwtInvalidatedListener fires with correct externalId
  • AND MORE TBD

Affected code checklist

  • Notifications
    • Display
    • Open
    • Push Processing
    • Confirm Deliveries
  • Outcomes
  • Sessions
  • In-App Messaging
  • REST API requests
  • Public API changes

Checklist

Overview

  • I have filled out all REQUIRED sections above
  • PR does one thing
  • Any Public API changes are explained in the PR details and conform to existing APIs

Testing

  • I have included test coverage for these changes, or explained why they are not needed
  • All automated tests pass, or I explained why that is not possible
  • I have personally tested this on my device, or explained why that is not possible

Final pass

  • Code is as readable as possible.
  • I have reviewed this PR myself, ensuring it meets each checklist item

nan-li added 11 commits March 19, 2026 13:00
Add operationJwt, operationExternalId, and requiresJwt to Operation
for multi-user JWT management. Override requiresJwt=false on
UpdateSubscriptionOperation since its backend endpoint has no JWT param.

Made-with: Cursor
- Stamp operationJwt/operationExternalId on operations at enqueue time
- Rewrite getNextOps: gate on isInitializedWithRemote, discard anonymous
  ops when IV is on, skip ops with null JWT (instead of blocking all)
- FAIL_UNAUTHORIZED: null JWT per-op, add unauthorizedRetries counter
  with max 3, fire jwtInvalidatedCallback with correct externalId
- Stamp JWT on follow-up operations from executors
- Add updateJwtForExternalId to update JWT on queued ops and wake queue
- Subscribe to ConfigModelStore to wake queue when remote params arrive
- Add listener management for IUserJwtInvalidatedListener on IOperationRepo

Made-with: Cursor
- updateUserJwt now calls operationRepo.updateJwtForExternalId to update
  JWT on all queued operations for the given externalId
- Delegate JWT invalidated listener add/remove to OperationRepo instead
  of UserManager

Made-with: Cursor
…delStore

Replace _identityModelStore.model.jwtToken with operation.operationJwt
in all 5 executors so each operation uses the JWT stamped at enqueue time
rather than the current user's JWT.

Made-with: Cursor
JWT invalidation is now handled by OperationRepo with the correct
per-operation externalId. Remove jwtInvalidatedCallback EventProducer,
jwtTokenInvalidated tracking, and JWT_TOKEN handling from onModelUpdated.
UserManager listener methods are now no-ops since OneSignalImp delegates
directly to OperationRepo.

Made-with: Cursor
Update existing JWT tests to match new per-operation behavior. Add tests:
- getNextOps skips null-JWT ops, allows requiresJwt=false through
- getNextOps discards anonymous ops when IV is on
- getNextOps returns null when isInitializedWithRemote is false
- getNextOps passes all ops when IV is off
- FAIL_UNAUTHORIZED nulls JWT and fires listener with correct externalId
- FAIL_UNAUTHORIZED drops ops after max retries
- updateJwtForExternalId updates JWT and resets retry count
- updateJwtForExternalId only affects matching user
- Enqueue stamps JWT/externalId from IdentityModelStore
- Follow-up operations inherit JWT/externalId from starting op

Made-with: Cursor
Executors were calling _identityModelStore.getIdentityAlias() to build
backend API URL paths, which returns the *current* user's alias. When
executing operations for a previous user, this caused the wrong user's
identity to be used in the URL (e.g., nan00's refresh-user fetching
nan01's data), resulting in 401 errors.

Replace with operation-derived alias: use operationExternalId when JWT
is present, otherwise fall back to onesignalId from the operation.

Made-with: Cursor
Test that each executor uses the operation's own JWT/externalId to
derive the identity alias for API calls, rather than reading from the
current user's IdentityModelStore. Covers both JWT-present (external_id
path) and no-JWT (onesignal_id path) scenarios for all 4 affected
executors: Identity, RefreshUser, Subscription, UpdateUser.

Made-with: Cursor
…WT presence

The identity alias derivation incorrectly used operationJwt != null to
decide between external_id and onesignal_id for the API URL path. This
is wrong because even if an operation has a JWT, if identity verification
is disabled on the server, the backend expects onesignal_id.

Now uses _configModelStore.model.useIdentityVerification as the
condition, which reflects the actual server configuration. Injected
ConfigModelStore into IdentityOperationExecutor and
UpdateUserOperationExecutor (the two that didn't have it). Updated all
unit tests accordingly.

Made-with: Cursor
@nan-li nan-li added the WIP Work In Progress label Mar 19, 2026
@nan-li nan-li changed the title Jwt fixes handle multi user fix(jwt): Multi-user fixes for identity verification - WIP for manual testing Mar 19, 2026
@nan-li nan-li force-pushed the jwt_fixes_handle_multi_user branch from 33e503a to 518f3e5 Compare March 19, 2026 20:10
@nan-li nan-li force-pushed the jwt_fixes_handle_multi_user branch from 518f3e5 to ca1dc4d Compare March 19, 2026 20:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

WIP Work In Progress

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant