Skip to content

feat(auth)!: stop persisting ID token; persist decoded profile instead#248

Merged
bmanquen merged 3 commits into
mainfrom
bm/refactor-auth
Jun 4, 2026
Merged

feat(auth)!: stop persisting ID token; persist decoded profile instead#248
bmanquen merged 3 commits into
mainfrom
bm/refactor-auth

Conversation

@bmanquen
Copy link
Copy Markdown
Collaborator

@bmanquen bmanquen commented Jun 2, 2026

  • Decode ID token once at sign-in to derive userInfo, then discard it

    • Persist the decoded profile (Zod-validated on read) so it survives reloads
    • Clear stored profile on sign-out and on unrecoverable session expiry
    • core: drop idToken from saveAuthData; add saveUserInfo/storedUserInfo and getStoredUserInfo()
    • hooks: remove AuthenticationState.idToken; provider rehydrates from stored profile
    • ui: update authenticated-user test helper

    BREAKING CHANGE: the ID token is no longer exposed or persisted.

    • Removed AuthenticationState.idToken (use userInfo for profile data)
    • Removed the YouVersionPlatformConfiguration.idToken getter
    • saveAuthData(accessToken, refreshToken, expiryDate) no longer accepts an idToken arg

Greptile Summary

This PR removes the raw ID token from the auth persistence layer: the token is now decoded once at sign-in to produce a Zod-validated user profile, which is persisted in place of the token itself. Token refresh no longer requires a stored ID token, sign-out clears the profile alongside the OAuth tokens, and a new host-controlled mode lets native Expo wrappers pass an externally-obtained profile directly into the provider.

  • YouVersionPlatformConfiguration: saveAuthData drops the idToken parameter; new saveUserInfo/storedUserInfo handle the decoded profile; clearAuthTokens now wipes the profile as well as the OAuth tokens.
  • YouVersionAPIUsers: handleAuthCallback persists the decoded profile after exchange and clears everything (tokens + profile) if an error is thrown; getStoredUserInfo() reads the validated profile from storage; refreshTokens no longer needs or preserves a stored ID token.
  • YouVersionAuthProvider: rehydrates userInfo via getStoredUserInfo() instead of re-decoding the token; adds a userInfo prop for host-controlled (native) auth flows.

Confidence Score: 4/5

Safe to merge after addressing the legacy idToken cleanup gap; all new auth paths are well-tested and logically sound.

The clearAuthTokens method no longer removes the idToken localStorage key. Existing users who signed in with the previous SDK version carry a raw ID token in storage; after upgrading and signing out, that key persists indefinitely because nothing in the new code touches it. The stated security goal of never keeping the signed token in storage is not met for this population until they uninstall or manually clear storage.

packages/core/src/YouVersionPlatformConfiguration.ts — the clearAuthTokens method needs a one-line migration to remove the legacy idToken key.

Important Files Changed

Filename Overview
packages/core/src/YouVersionPlatformConfiguration.ts Replaces idToken getter/storage with saveUserInfo/storedUserInfo (Zod-validated); clearAuthTokens no longer removes the legacy idToken localStorage key, leaving it in storage for existing users after upgrade
packages/core/src/Users.ts handleAuthCallback now persists decoded profile instead of idToken; catch block correctly calls clearAuthTokens() before re-throwing; getStoredUserInfo() guards on id presence; refreshTokens() leaves the stored profile untouched
packages/core/src/schemas/user-info.ts New Zod schema for the persisted profile; all fields optional (empty object passes), but getStoredUserInfo() adds an id-presence guard to prevent empty stubs from counting as authenticated
packages/hooks/src/context/YouVersionAuthProvider.tsx Provider now rehydrates userInfo via getStoredUserInfo() instead of re-decoding idToken; adds host-controlled mode (userInfo prop) for Expo native sign-in bridging; two useEffects cleanly separated by isHostControlled guard
packages/hooks/src/useYVAuth.ts Removes idToken from authTokens memo and AuthenticationState; isAuthenticated still derives from userInfo presence; no functional regressions
packages/ui/src/components/bible-reader.tsx Adds onSignInPress/onSignOutPress props to BibleReader.Root, bridged to native handlers in UserMenu; falls back to web SDK auth when callbacks are absent
packages/ui/src/test/utils.ts Updated setupAuthenticatedUser to use saveUserInfo + getStoredUserInfo mock instead of idToken; refreshTokenIfNeeded now returns true (matching the new refreshed=true to getStoredUserInfo path)

Comments Outside Diff (2)

  1. packages/core/src/Users.ts, line 192-201 (link)

    P2 extractSignInResult still attaches idToken to the result — partial removal of the exposure path

    handleAuthCallback no longer persists the ID token, but it still returns a SignInWithYouVersionResult whose idToken field is set (via extractSignInResult). processCallback() in useYVAuth returns this object directly, so any consumer calling result.idToken can still read the raw token from memory. If the intent is to fully remove the exposure vector, idToken should be omitted from the result constructed inside extractSignInResult.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: packages/core/src/Users.ts
    Line: 192-201
    
    Comment:
    **`extractSignInResult` still attaches `idToken` to the result — partial removal of the exposure path**
    
    `handleAuthCallback` no longer persists the ID token, but it still returns a `SignInWithYouVersionResult` whose `idToken` field is set (via `extractSignInResult`). `processCallback()` in `useYVAuth` returns this object directly, so any consumer calling `result.idToken` can still read the raw token from memory. If the intent is to fully remove the exposure vector, `idToken` should be omitted from the result constructed inside `extractSignInResult`.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code Fix in Cursor Fix in Codex

  2. packages/core/src/Users.ts, line 148-154 (link)

    P2 Catch block doesn't clean up saveAuthData / saveUserInfo on error

    If an exception is raised after saveAuthData and saveUserInfo have already written to localStorage (e.g. window.history.replaceState throwing in a restrictive browser environment), the catch block removes the PKCE/OAuth temporary keys but leaves the persisted tokens and user profile intact. On the next page load the user would be treated as authenticated, while the provider also exposes the error that was thrown. Adding YouVersionPlatformConfiguration.clearAuthTokens() at the top of the catch block would make the failure path consistent with sign-out semantics.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: packages/core/src/Users.ts
    Line: 148-154
    
    Comment:
    **Catch block doesn't clean up `saveAuthData` / `saveUserInfo` on error**
    
    If an exception is raised after `saveAuthData` and `saveUserInfo` have already written to localStorage (e.g. `window.history.replaceState` throwing in a restrictive browser environment), the catch block removes the PKCE/OAuth temporary keys but leaves the persisted tokens and user profile intact. On the next page load the user would be treated as authenticated, while the provider also exposes the error that was thrown. Adding `YouVersionPlatformConfiguration.clearAuthTokens()` at the top of the catch block would make the failure path consistent with sign-out semantics.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code Fix in Cursor Fix in Codex

Fix All in Claude Code Fix All in Cursor Fix All in Codex

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
packages/core/src/YouVersionPlatformConfiguration.ts:73-76
Legacy `idToken` localStorage key is never removed after upgrading. The old `saveAuthData` always removed `idToken` (passing null set `removeItem`), but the new signature dropped that branch entirely. Users who signed in with the previous version have a raw ID token sitting in `localStorage`; after upgrading they sign out, `clearAuthTokens` removes `accessToken`, `refreshToken`, `expiryDate`, and `userInfo` — but the `idToken` key is silently left behind. That directly contradicts this PR's stated goal of never keeping the signed token in storage.

```suggestion
  public static clearAuthTokens(): void {
    this.saveAuthData(null, null, null);
    this.saveUserInfo(null);
    // Migration: remove the legacy idToken key written by older versions of this SDK.
    // saveAuthData no longer touches this key, so it must be cleaned up explicitly.
    localStorage.removeItem('idToken');
  }
```

### Issue 2 of 2
packages/ui/src/components/bible-reader.tsx:423-436
Unnecessary `else` blocks after a return-like branch. Per the project style guide, prefer early returns to reduce nesting. Both handlers can be flattened by calling the host callback and returning early.

```suggestion
  const handleSignIn = () => {
    if (onSignInPress) {
      onSignInPress();
      return;
    }
    void signIn({ scopes: ['profile'] });
  };
  const handleSignOut = () => {
    if (onSignOutPress) {
      onSignOutPress();
      return;
    }
    void signOut();
  };
```

Reviews (3): Last reviewed commit: "fix(auth): harden sign-in result and cal..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jun 2, 2026

🦋 Changeset detected

Latest commit: d2e7621

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@youversion/platform-core Major
@youversion/platform-react-hooks Major
@youversion/platform-react-ui Major
vite-react Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Comment thread packages/core/src/schemas/user-info.ts
@cameronapak cameronapak changed the title feat(auth)!: stop persisting ID token; persist decoded profile instead WIP: feat(auth)!: stop persisting ID token; persist decoded profile instead Jun 2, 2026
@bmanquen bmanquen force-pushed the bm/refactor-auth branch from ca7654f to a4ac0a4 Compare June 3, 2026 11:07
@bmanquen bmanquen changed the title WIP: feat(auth)!: stop persisting ID token; persist decoded profile instead feat(auth)!: stop persisting ID token; persist decoded profile instead Jun 3, 2026
Copy link
Copy Markdown
Collaborator

@cameronapak cameronapak left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment thread packages/core/src/YouVersionPlatformConfiguration.ts
Comment thread packages/core/src/schemas/user-info.ts
cameronapak
cameronapak previously approved these changes Jun 3, 2026
Copy link
Copy Markdown
Collaborator

@cameronapak cameronapak left a comment

Choose a reason for hiding this comment

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

Please consider the feedback from DeepWiki and from Greptile. This is approved, but please, please, please do not merge the changeset PR after this yet. If we're making a breaking major change bump, I have some things I need to prep in another PR that would merge alongside it

bmanquen added 2 commits June 4, 2026 11:18
  - Decode ID token once at sign-in to derive userInfo, then discard it
  - Persist the decoded profile (Zod-validated on read) so it survives reloads
  - Clear stored profile on sign-out and on unrecoverable session expiry
  - core: drop idToken from saveAuthData; add saveUserInfo/storedUserInfo and getStoredUserInfo()
  - hooks: remove AuthenticationState.idToken; provider rehydrates from stored profile
  - ui: update authenticated-user test helper

  BREAKING CHANGE: the ID token is no longer exposed or persisted.
  - Removed AuthenticationState.idToken (use userInfo for profile data)
  - Removed the YouVersionPlatformConfiguration.idToken getter
  - saveAuthData(accessToken, refreshToken, expiryDate) no longer accepts an idToken arg
  - Add optional `userInfo` prop to YouVersionProvider and YouVersionAuthProvider
    so a host can own sign-in and pass the resulting profile down
  - When `userInfo` is provided (including `null`), skip the web token/OAuth init
    and drive `userInfo`/`isAuthenticated` from the supplied profile
  - Construct `YouVersionUserInfo` from the host-supplied `YouVersionUserInfoJSON`
  - Add `onSignInPress`/`onSignOutPress` callbacks to BibleReader Root and UserMenu
  - Bridge sign-in/out to host-supplied native actions (e.g. Expo PKCE), falling
    back to the Web SDK's own `signIn`/`signOut` when not provided
…tate

  - Require `id` on stored profiles: getStoredUserInfo() returns null when
    the persisted profile lacks an `id`, so a tampered/empty `{}` can't flip
    isAuthenticated true with an empty profile (schema stays all-optional to
    match the Swift SDK)
  - Stop attaching the raw ID token to SignInWithYouVersionResult: remove
    `idToken` from extractSignInResult and drop the field from the
    SignInWithYouVersionResult type/constructor, closing the in-memory
    exposure path for callback consumers
  - Clear persisted tokens and profile on callback failure: handleAuthCallback
    now calls clearAuthTokens() in its catch block so an error after
    saveAuthData/saveUserInfo can't leave the user looking signed in
  - Update tests/mocks to drop the removed `idToken` field
@bmanquen bmanquen force-pushed the bm/refactor-auth branch from 818528e to d2e7621 Compare June 4, 2026 16:46
Comment thread packages/core/src/YouVersionPlatformConfiguration.ts
@bmanquen bmanquen merged commit b8309a4 into main Jun 4, 2026
6 checks passed
@bmanquen bmanquen deleted the bm/refactor-auth branch June 4, 2026 16:59
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.

2 participants