Skip to content

fix(react): drop cached JWT when sessionId rotates#329

Open
ramonclaudio wants to merge 1 commit into
get-convex:mainfrom
ramonclaudio:fix/react-cached-token-on-session-rotation
Open

fix(react): drop cached JWT when sessionId rotates#329
ramonclaudio wants to merge 1 commit into
get-convex:mainfrom
ramonclaudio:fix/react-cached-token-on-session-rotation

Conversation

@ramonclaudio

@ramonclaudio ramonclaudio commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

cachedToken in ConvexBetterAuthProvider doesn't get cleared when the session id changes (only on logout). So right after a session rotation (password change), the next call to fetchAccessToken({ forceRefreshToken: false }) hands back the JWT for the now-deleted session.

This PR backs cachedToken with a ref so the fetcher reads the current value instead of the one captured at the last render. The rotation check runs inline in the fetcher and clears the ref when sessionId changes. A promise-identity guard in .then/.catch/.finally keeps a late-resolving stale token() fetch from overwriting a fresh value.

Unit test repro: 6/9 fail on v0.12.2, all pass with this fix.

Worth flagging: this doesn't close the visible Invalid session ID flicker on changePassword({ revokeOtherSessions: true }). Tested end-to-end on repros with real credentials (Expo test repro, TanStack test repro) but the patched build behaves the same as vanilla on that flow. Looks like an upstream Convex issue, same shape as convex-js#82 but I am honestly not 100% sure. better-auth#9345 is the upstream complement on the Better Auth side that would preserve the caller's session on change-password instead of rotating it.

@vercel

vercel Bot commented Apr 10, 2026

Copy link
Copy Markdown

@ramonclaudio is attempting to deploy a commit to the Convex Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Apr 10, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The hook useUseAuthFromBetterAuth mirrors cachedToken into cachedTokenRef via a setCachedToken helper so fetchAccessToken reads the latest token. It clears the cached JWT when the Better Auth session disappears and adds lastSessionIdRef plus an effect that, on sessionId rotation, clears cachedToken and nulls pendingTokenRef.current to drop in-flight fetches tied to the old session. fetchAccessToken now prefers cachedTokenRef, reuses in-flight promises, and only updates the cache when the resolved promise is still the active one.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant useUseAuthFromBetterAuth
  participant fetchAccessToken
  participant TokenEndpoint
  Client->>useUseAuthFromBetterAuth: needs authenticated request
  useUseAuthFromBetterAuth->>fetchAccessToken: fetchAccessToken()
  fetchAccessToken->>useUseAuthFromBetterAuth: read cachedTokenRef.current
  alt cached token valid
    fetchAccessToken->>Client: return cached token
  else no valid token
    fetchAccessToken->>useUseAuthFromBetterAuth: set pendingTokenRef
    fetchAccessToken->>TokenEndpoint: request new JWT
    TokenEndpoint-->>fetchAccessToken: JWT response
    fetchAccessToken->>useUseAuthFromBetterAuth: setCachedToken(JWT)
    useUseAuthFromBetterAuth->>Client: return JWT
  end
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers

  • erquhart
  • devstojko
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main fix: clearing the cached JWT when sessionId rotates.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description accurately describes the cacheing bug and the ref-based solution with promise guards, matching the changeset's session-rotation handling.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ramonclaudio ramonclaudio force-pushed the fix/react-cached-token-on-session-rotation branch from 5cad276 to 50dea0d Compare April 21, 2026 15:11
ramonclaudio added a commit to ramonclaudio/convex-better-auth-fork that referenced this pull request May 8, 2026
The bridge's fetchAccessToken called setCachedToken inside its
`/convex/token` .then chain. The setState scheduled a re-render that
landed inside Convex's fetchTokenAndGuardAgainstRace await window
(authentication_manager.ts), bumped configVersion, and made the
in-flight setConfig bail with isFromOutdatedConfig:true. The WebSocket
stayed paused and useConvexAuth().isAuthenticated never settled to
true on Hermes V1 native async (Expo SDK 56 canary 2026-05-05+ since
expo/expo#45345 dropped @babel/plugin-transform-async-to-generator
from the Hermes V1 preset).

Drop the async keyword and wrap the body in new Promise(...). The
wrapping Promise resolves via thenable adoption, which inserts the
same microtask hop the regenerator runtime's _asyncToGenerator
provides. The hop pushes the React re-render past the consumer's
await continuation, so the next setAuth fires after setConfig
completes instead of inside its await window.

Refs:
- get-convex#168 (Expo, open since Nov 2025)
- get-convex#303 (Next.js, open since Mar 2026)
- get-convex#329 (closed; addressed session-rotation
  leg of the same race class)
- get-convex#346 (closed; iOS background resume)
ramonclaudio added a commit to ramonclaudio/convex-better-auth-fork that referenced this pull request May 8, 2026
… on Hermes V1

The /convex/token response triggers a session rotation (via Better
Auth's Set-Cookie processing) plus a setCachedToken call inside the
bridge's .then. The next render rebuilds fetchAccessToken's
useCallback (keyed on [sessionId]) and fires
ConvexAuthStateFirstEffect's client.setAuth a second time.

On Hermes V1 native async (Expo SDK 56 canary 2026-05-05+ since
expo/expo#45345 dropped @babel/plugin-transform-async-to-generator),
that second setAuth lands inside the first setConfig's await window
in authentication_manager.ts. fetchTokenAndGuardAgainstRace bumps
configVersion on entry and the original await sees the stale value,
returning isFromOutdatedConfig: true. setConfig bails without
resumeSocket() and the chain repeats.

Drop the async keyword and wrap the body in new Promise(executor)
directly. The constructor's resolve(thenable) schedules a
NewPromiseResolveThenableJob microtask, the same hop regenerator's
_asyncToGenerator provides. With the hop in place the second setAuth
lands after the first setConfig finishes rather than during its
await window.

Refs:
- get-convex#168 (Expo, open since Nov 2025)
- get-convex#303 (Next.js, open since Mar 2026)
- get-convex#329 (closed; addressed session-rotation
  leg of the same race class)
- get-convex#346 (closed; iOS background resume)
@ramonclaudio ramonclaudio changed the title fix(react): invalidate cachedToken on session rotation, not just logout fix(react): drop cached JWT when sessionId rotates May 19, 2026
@ramonclaudio ramonclaudio reopened this May 19, 2026
@ramonclaudio ramonclaudio force-pushed the fix/react-cached-token-on-session-rotation branch from 1b21b5a to 2ed5be1 Compare May 19, 2026 16:09

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/react/index.tsx`:
- Around line 140-154: The effect drops the pendingTokenRef pointer but doesn't
prevent an already-started fetchAccessToken promise from later calling
setCachedToken and restoring a stale JWT; change the
fetchAccessToken/pendingTokenRef flow so each token request carries a unique
request id or the promise object and, before calling setCachedToken(token),
verify that pendingTokenRef.current still matches that id/promise (or that a
monotonic tokenRequestVersion equals the version captured at start). Also, when
the session rotates in the useEffect (where lastSessionIdRef and pendingTokenRef
are handled), increment/clear that version or set pendingTokenRef to a sentinel
so any in-flight .then handlers will detect the mismatch and abort calling
setCachedToken; update places that assign/clear pendingTokenRef and the
.then/.catch handlers in fetchAccessToken to perform this guard.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: get-convex/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: abee74a9-72d1-4c6d-9c8e-a05779a27311

📥 Commits

Reviewing files that changed from the base of the PR and between 50dea0d and 1b21b5a.

📒 Files selected for processing (1)
  • src/react/index.tsx

Comment thread src/react/index.tsx Outdated
@ramonclaudio ramonclaudio force-pushed the fix/react-cached-token-on-session-rotation branch 5 times, most recently from 5fdf934 to 9469884 Compare May 20, 2026 16:36
@ramonclaudio

ramonclaudio commented May 20, 2026

Copy link
Copy Markdown
Contributor Author

Hey @erquhart, sorry for the multiple pings here.

Just did a bunch of testing so I could provide useful repros. The unit tests show this fix works at the React layer (6/9 fail without it, all pass with it). But the Expo and TanStack real-app repros show the user-visible Invalid session ID flicker on changePassword is the same with or without this PR. So this PR alone doesn't close the visible window. It might actually be an upstream Convex issue (convex-js#82), I'm not really sure here.

Curious for your read on the upstream side (better-auth#9345). I opened this PR in Better Auth a few weeks ago after closing this one initially. Is preserving the caller's session on change-password the right call, or is the rotation actually the right behavior and users should just ignore the flicker?

If neither direction feels right, happy to close this and #9345. Don't want to pile on review work.

ramonclaudio added a commit to ramonclaudio/convex-better-auth-329-tanstack-repro that referenced this pull request May 20, 2026
@ramonclaudio ramonclaudio force-pushed the fix/react-cached-token-on-session-rotation branch from 9469884 to 37bdd4b Compare June 2, 2026 20:19
@pkg-pr-new

pkg-pr-new Bot commented Jun 16, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@convex-dev/better-auth@329

commit: 37bdd4b

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