Skip to content

feat: [JWT-4] identity model JWT, the JWT repo, and the public API - #1709

Open
nan-li wants to merge 2 commits into
nan/jwt-pr3-iv-gatefrom
nan/jwt-pr4-identity-api
Open

feat: [JWT-4] identity model JWT, the JWT repo, and the public API#1709
nan-li wants to merge 2 commits into
nan/jwt-pr3-iv-gatefrom
nan/jwt-pr4-identity-api

Conversation

@nan-li

@nan-li nan-li commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Description

One Line Summary

Add the Identity Verification public surface: JWT on the identity model, the JWT repo, and the app-facing API to supply a token and hear when it was invalidated.

Details

Motivation

Under Identity Verification, an app must hand the SDK a JWT for a user and learn when that token stopped being accepted. The SDK needs per–external-ID storage and ask-once semantics so the app is prompted once per user, not once per rejected request — including when a listener registers after start or login.

Scope

  • OSIdentityModel / OSIdentityModelRepo — JWT field and persistence the public API needs
  • OSUserJwtRepo — token per external ID; ask-once; late-listener catch-up
  • OSUserJwtInvalidatedEvent + public API (updateUserJwt, addUserJwtInvalidatedListener, …)
  • Removes unused beta surface: requiresUserAuth, onJwtExpired, and related handler typealiases
  • Unit tests for identity model JWT, JWT repo, and public API
  • No request-pipeline auth, queue release on storeJwt, IAM, or demo UI (later PRs)

Stacked on #1707 (nan/jwt-pr3-iv-gate).

Testing

Unit testing

  • OSIdentityModelTests
  • OSIdentityModelRepoTests
  • OSUserJwtRepoTests
  • UserJwtApiTests

Manual testing

Built in sequence as part of the local JWT stack against an iOS Simulator; this PR’s commit compiled on top of PR3.

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

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multimodal adversarial review (interrogate)

Skill: Cursor interrogate (pstack). Reviewers: claude-fable-5-thinking-xhigh, gpt-5.6-sol-xhigh, cursor-grok-4.5-high-fast, claude-opus-5-thinking-high. Lead judgment applied below — not a raw dump.

Intent

PR4 of the IV/JWT stack (on PR3). Add the Identity Verification surface an app talks to: supply a JWT for a user (updateUserJwt / login token), and get told when that token stopped being accepted (OSUserJwtRepo ask-once + late-listener catch-up + public listener API). Persist JWT fields on OSIdentityModel / repo with compare-and-swap invalidate. Remove unused beta surface (requiresUserAuth, onJwtExpired). Request-pipeline / queue release intentionally later.

Reviewers

  • A: claude-fable-5-thinking-xhigh — 11 findings
  • B: gpt-5.6-sol-xhigh — 6 findings
  • C: cursor-grok-4.5-high-fast — 8 findings
  • D: claude-opus-5-thinking-high — 14 findings

Act On

  1. login(externalId:token:) bypasses storeJwt / ask re-arm (A/B/C/D)storeJwt claims every app-supplied token arrives there, but login/createNewUser still assign jwtBearerToken directly. That never clears askedForToken, so answering an invalidation via login permanently silences later asks. Anonymous→identify also parks the token on the anonymous model while the new external-ID model stays tokenless, so validJwt(externalId:) is nil right after the primary IV login path. Same-user re-login early-returns and drops a refreshed token entirely.
  2. Raw JWT logged in updateUserJwt (A/B/C/D) — Author TODO says omit before shipping; OSLogListeners still receive every level regardless of _nsLogLevel. Strip now.
  3. Bearer token persisted to shared UserDefaults; secure-storage TODO deleted (A/B/C/D) — Encode path newly archives the live credential via OSModelStore → app-group defaults. At minimum restore a tracked TODO / keep real tokens memory-only and persist only the invalid sentinel; Keychain if cross-launch persistence is required.
  4. No login(..., token:) coverage against the new JWT surface (A/C/D) — Repo/API tests seed models with externalId already set; they cannot catch (1). Add anonymous→identify, create-user, and same-user refresh cases asserting userJwtRepo.validJwt + ask re-arm.

Consider

  • Ask/token TOCTOU (A/B/C/D)invalidateJwt’s nil-check then askForToken is not atomic with updateJwt; concurrent replacement can leave a valid token + stuck ask.
  • Late-listener catch-up (A/B/C/D) — Direct strong capture + main async bypasses OSObservable weak/remove semantics; concurrent ask can double-deliver.
  • set(property: OS_JWT_BEARER_TOKEN) without preventServerUpdate: true (A/D) — Today alias cast fails; prefer explicit opt-out like OSSubscriptionModel.
  • askedForToken survives logout (D) — Late listeners can be told to mint for a logged-out user.
  • Silent drop when no model matches externalId (D) — Void API + ask uncleared is terminal for the session during clearData hydration windows.

Noted

  • OSUserJwtProviding / unread identityVerificationService look like stacked-PR scaffolding (A/D).
  • In-band OS_JWT_TOKEN_INVALID string checked at three layers (A/D) — fine for now; tighten when executors land.
  • Lazy observer init race (B) — low practical risk for UI registration.

Dismissed

  • Full JWT-state enum rewrite as a blocker for this PR — design preference; not required to ship the public surface if login routing + logging/persistence are fixed.
  • Sticky-event observable redesign for catch-up — nicer long-term, overkill if membership/weak capture is fixed.
  • Wrapper changelog as a hard block — PR body already documents beta API migration; still worth a release note.

Agreement Map

All four models independently flagged login-vs-storeJwt, JWT-in-logs, and plaintext persistence. Catch-up + ask/token races were also consensus (lower severity). D pushed hardest on silent drops / logout ask lifecycle; B on log-listener fanout and lazy observer init. Highest-confidence blockers are the login ingress contract break and shipping secrets to logs/disk.

Open in Web View Automation 

Sent by Cursor Automation: Untitled

Comment thread iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModel.swift
Comment thread iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModel.swift Outdated
Comment thread iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSUserJwtRepo.swift
@nan-li nan-li changed the title feat: [PR4] identity model JWT, the JWT repo, and the public API feat: [JWT-2] identity model JWT, the JWT repo, and the public API Aug 12, 2026
@nan-li nan-li changed the title feat: [JWT-2] identity model JWT, the JWT repo, and the public API feat: [JWT-4] identity model JWT, the JWT repo, and the public API Aug 12, 2026
The surface an app talks to under Identity Verification: it hands the SDK
a token for a user, and the SDK tells it when that token stopped being
accepted.

OSUserJwtRepo holds the token per external ID and remembers who has
already been asked for one, so an app is asked once per user rather than
once per rejected request. A listener registered after start or login
still hears about an ask that already fired, since the alternative is an
app that never learns it owes a token for the user it just logged in.

Removes the beta JWT surface this replaces: requiresUserAuth, which
nothing ever read, along with onJwtExpired and its handler typealiases.
Anyone on the beta JWT API moves to addUserJwtInvalidatedListener and
updateUserJwt.

storeJwt lands here in the form the public API needs; the later PR that
introduces the queues extends it to release the work held for want of a
token.

Co-authored-by: Cursor <cursoragent@cursor.com>
@nan-li
nan-li force-pushed the nan/jwt-pr4-identity-api branch from 03f2ebf to 2f3cd34 Compare August 12, 2026 16:57
Use preventServerUpdate so the token never becomes an identity delta by
accident of the alias cast failing.

Co-authored-by: Cursor <cursoragent@cursor.com>
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