Skip to content

feat(iv): attach JWT in HTTP client and operation executors (4/6)#2626

Open
nan-li wants to merge 2 commits intofeat/iv-queue-runtime-03from
feat/iv-http-executors-04
Open

feat(iv): attach JWT in HTTP client and operation executors (4/6)#2626
nan-li wants to merge 2 commits intofeat/iv-queue-runtime-03from
feat/iv-http-executors-04

Conversation

@nan-li
Copy link
Copy Markdown
Contributor

@nan-li nan-li commented Apr 24, 2026

Fourth of six PRs re-implementing Identity Verification (IV) on the feat/identity_verification_feature_flagged_major_release integration branch.

Stack: #2623#2624#2625this → (5/6) → (6/6)

What this PR does

Plumbs the JWT all the way from storage to the HTTP wire, and wraps all seven user-facing operation executors in the new-code-path extension pattern established in #2625.

HTTP layer (unconditional plumbing)

  • OptionalHeaders.jwt: String? field.
  • HttpClient sets Authorization: Bearer <jwt> when non-null.
  • logHTTPSent redacts Authorization defensively (the log call currently happens before the header is set, but protecting against future reordering).

Backend service signatures

  • jwt: String? = null default param added to 10 methods across 4 services:
    • IIdentityBackendService.setAlias / deleteAlias
    • IUserBackendService.createUser / updateUser / getUser
    • ISubscriptionBackendService.createSubscription / updateSubscription / deleteSubscription / transferSubscription
    • ICustomEventBackendService.sendCustomEvent
  • Each impl threads jwt into OptionalHeaders. Existing callers unchanged.
  • Excluded: getIdentityFromSubscription. That endpoint is not allowed when jwt_required=true; LoginUserFromSubscriptionOperationExecutor already returns FAIL_NORETRY under IV, so the call never reaches the backend service on the IV path. KDoc added to document this.

Executor gating — extension pattern

New file ExecutorsIvExtensions.kt exposes:

  • IvBackendParams(aliasLabel, aliasValue, jwt) with legacyFor(onesignalId) factory.
  • resolveIvBackendParams(op, onesignalId, jwtTokenStore) — alias + JWT for backend calls.
  • resolveIvJwt(op, jwtTokenStore) — JWT-only for calls that don't take alias label/value.
  • shouldFailLoginUserFromSubscription() — IV-active predicate.

Each helper short-circuits on IdentityVerificationGates.ivBehaviorActive == false. Base-class dispatch sites gate on IdentityVerificationGates.newCodePathsRun, so:

  • Phase 1 (newCodePathsRun=false): extensions never called; executors byte-for-byte identical to today.
  • Phase 3 (newCodePathsRun=true, ivBehaviorActive=false): extensions run and return legacy values; new code path exercised without IV behavior — validates structural changes.
  • IV active: alias switches to external_id, JWT attaches.

Seven executors wired:

Executor Resolution
LoginUserOperationExecutor resolveIvJwt (createUser uses identities map, no alias)
IdentityOperationExecutor resolveIvBackendParams (setAlias / deleteAlias)
SubscriptionOperationExecutor.createSubscription resolveIvBackendParams
SubscriptionOperationExecutor.updateSubscription / deleteSubscription resolveIvJwt
SubscriptionOperationExecutor.transferSubscription resolveIvBackendParams
UpdateUserOperationExecutor resolveIvBackendParams (externalId pulled from operations.first(); grouped ops share user)
RefreshUserOperationExecutor resolveIvBackendParams
LoginUserFromSubscriptionOperationExecutor shouldFailLoginUserFromSubscriptionFAIL_NORETRY
CustomEventOperationExecutor resolveIvJwt

DI auto-wires JwtTokenStore into executors through constructor reflection — no module changes needed (store was registered in #2623).

Tests

  • New ExecutorsIvExtensionsTests — 9 focused unit tests across Phase 1 / Phase 3 / IV-active for both resolveIvBackendParams and resolveIvJwt, plus shouldFailLoginUserFromSubscription.
  • New HttpClientTests cases — Authorization header set iff OptionalHeaders.jwt != null.
  • New IdentityOperationExecutorTests cases — IV-active and Phase 3 end-to-end verifies that the correct alias + JWT reach the backend mock.
  • Updated existing test mocks in 4 backend test files + 6 executor test files to match new three-arg HTTP calls / new constructor params.
  • New ExecutorMocks.getJwtTokenStore() helper (real empty JwtTokenStore backed by MockPreferencesService).

Test plan

  • ./gradlew :OneSignal:core:testDebugUnitTest — 814 pass / 2 fail (both pre-existing SDKInitTests failures, unrelated; reproduced on baseline).
  • :OneSignal:in-app-messages, :OneSignal:notifications, :OneSignal:location all compile clean.
  • Manual smoke: verify Authorization: Bearer <jwt> is present on network requests when a customer has jwt_required=true (will be testable once PR 5 wires the public login(externalId, jwt) API).

Out of scope (deferred to PR 5)

  • IAM / InAppBackendService JWT integration
  • RYW plumbing on CreateUserResponse / JSONConverter
  • IAM 401/403 retry via IJwtUpdateListener
  • JwtTokenStore.pruneToExternalIds caller (logout path)

🤖 Generated with Claude Code

HTTP layer:
- OptionalHeaders.jwt; HttpClient sets Authorization: Bearer header when
  non-null. Defensive redaction of Authorization in logHTTPSent.

Backend services (unconditional jwt: String? = null plumbing):
- IIdentityBackendService.setAlias / deleteAlias
- IUserBackendService.createUser / updateUser / getUser
- ISubscriptionBackendService.createSubscription / updateSubscription
  / deleteSubscription / transferSubscription
- ICustomEventBackendService.sendCustomEvent
- getIdentityFromSubscription deliberately excluded: endpoint is not
  allowed when jwt_required=true; LoginUserFromSubscription executor
  already returns FAIL_NORETRY under IV.

Executor gating via extension pattern (matches PR 2625 precedent):
- New file ExecutorsIvExtensions.kt exposes resolveIvBackendParams,
  resolveIvJwt, shouldFailLoginUserFromSubscription. Outer dispatch
  at each base executor checks IdentityVerificationGates.newCodePathsRun;
  inner checks ivBehaviorActive to keep Phase 3 users (new code path
  on, IV behavior off) on legacy alias/jwt values byte-for-byte.
- 7 executors dispatch around alias + JWT resolution:
  LoginUser, Identity, Subscription (Create/Update/Delete/Transfer),
  UpdateUser, RefreshUser, LoginUserFromSubscription, CustomEvent.
- LoginUserFromSubscription short-circuits FAIL_NORETRY when IV active.

DI wires automatically via constructor reflection; no module changes.

Tests:
- ExecutorsIvExtensionsTests: 9 focused tests covering all three phases.
- IdentityOperationExecutor: IV-active and Phase 3 integration tests.
- HttpClient: Authorization header presence/absence.
- Backend service test mocks updated for the new three-arg http calls.
- ExecutorMocks gained getJwtTokenStore() helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…UTHORIZED

Three bugs surfaced by bot review on PR 2626:

1. HttpClient: set all optional headers (cacheKey, rywToken, retryCount,
   sessionDuration, jwt) BEFORE the body write. On real Android,
   `getOutputStream()` triggers `connect()`, after which
   `setRequestProperty` throws or is silently dropped. Pre-PR this was
   latent because OptionalHeaders was only passed on GET. This PR wires
   it through POST/PATCH/DELETE, activating the bug for every
   authenticated call. Matches the fix from old IV branch commit
   1dab8e0.

   MockHttpURLConnection now simulates the real contract: `connect`,
   `getOutputStream`, and `getResponseCode` flip `headersCommitted=true`,
   and `setRequestProperty` throws after that. A new HttpClientTests
   case (POST + JWT) now regression-guards the ordering fix.

2. LoginUserOperationExecutor: under `ivBehaviorActive`, skip the
   optimistic inline SetAliasOperation and go straight to createUser.
   The inline op identifies the target by `onesignal_id =
   existingOnesignalId`, but IV's alias resolution rewrites the call
   to `external_id = newExternalId` — targeting a user that doesn't
   exist yet. 404 -> FAIL_NORETRY -> fallthrough to createUser
   orphans the anonymous user; or a 200 idempotent-PATCH against a
   different pre-existing user corrupts local identity. createUser's
   upsert semantics handle the merge correctly via the identities map.

3. SubscriptionOperationExecutor: add UNAUTHORIZED -> FAIL_UNAUTHORIZED
   to update/delete/transfer catch blocks. `createSubscription` handles
   it correctly; the other three fall through to `else -> FAIL_NORETRY`
   which swallows 401s without invalidating the stored JWT or firing
   `jwtInvalidatedHandler`. Pre-existing asymmetry that this PR
   activates by wiring JWT into these endpoints.

Tests:
- HttpClientTests: POST + JWT doesn't throw (ordering enforced by mock).
- LoginUserOperationExecutorTests: IV active + existingOnesignalId +
  externalId -> createUser; IdentityOperationExecutor not invoked.
- SubscriptionOperationExecutorTests: update/delete/transfer each
  return FAIL_UNAUTHORIZED on 401 under IV.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@nan-li
Copy link
Copy Markdown
Contributor Author

nan-li commented Apr 24, 2026

@claude review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant