Skip to content

feat(clerk-js): Monotonic token replacement based on oiat#8097

Draft
nikosdouvlis wants to merge 2 commits intonikos/plat-2469-oiat-jwt-header-typefrom
nikos/session-minter-sdk-changes
Draft

feat(clerk-js): Monotonic token replacement based on oiat#8097
nikosdouvlis wants to merge 2 commits intonikos/plat-2469-oiat-jwt-header-typefrom
nikos/session-minter-sdk-changes

Conversation

@nikosdouvlis
Copy link
Member

Why

With Session Minter, edge-minted tokens can have fresh iat (just minted) but stale claims (copied from an old parent). In multi-tab scenarios, a background tab's stale edge token can overwrite a fresher DB-minted token in the __session cookie, causing claim regression.

The old broadcast guard compared iat, which doesn't reflect claim freshness for edge tokens.

What

Introduces oiat ?? iat as the unified claim freshness metric:

  • Token with oiat (header): oiat = when claims were last read from DB
  • Token without oiat: origin-minted (coupled FF), so iat = when claims were last read from DB

New shared comparator (tokenFreshness.ts) used across 4 guard points:

  1. tokenCache.ts handleBroadcastMessage - replaces the old iat comparison
  2. tokenCache.ts setInternal - async compare-and-swap at resolve time
  3. Session.ts #dispatchTokenEvents - before token:update emit
  4. AuthCookieService.ts updateSessionCookie - cookie chokepoint with session scoping

Guard 4 is the most important. It compares against the actual __session cookie value (shared state), not in-memory state. This catches the sleeping tab edge case where a tab wakes up with a stale baseline, fetches an edge token, and tries to overwrite a fresher cookie that another tab maintained.

The cookie guard scopes by sid so session switches (different oiat timelines) always pass through.

Full decision table and edge case analysis in docs/plans/session-minter-sdk-changes.md.

Stacks on #8096.

Test plan

  • Broadcast: edge token with fresh iat but stale oiat is rejected
  • Broadcast: tokens without oiat fall back to iat comparison
  • Cache setInternal: async race, fresher resolve wins
  • Session dispatch: stale token doesn't overwrite lastActiveToken
  • Cookie guard: stale oiat blocked within same session
  • Cookie guard: different session always allowed (session switch)
  • Cookie guard: sleeping tab scenario (in-memory stale, cookie fresh)
  • Cookie guard: iat fallback when oiat missing
  • First mint with no existing token passes unconditionally
  • Comparator: all 15 decision table rows

@changeset-bot
Copy link

changeset-bot bot commented Mar 17, 2026

🦋 Changeset detected

Latest commit: 91b664b

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

This PR includes changesets to release 21 packages
Name Type
@clerk/clerk-js Patch
@clerk/shared Patch
@clerk/chrome-extension Patch
@clerk/expo Patch
@clerk/agent-toolkit Patch
@clerk/astro Patch
@clerk/backend Patch
@clerk/expo-passkeys Patch
@clerk/express Patch
@clerk/fastify Patch
@clerk/hono Patch
@clerk/localizations Patch
@clerk/msw Patch
@clerk/nextjs Patch
@clerk/nuxt Patch
@clerk/react-router Patch
@clerk/react Patch
@clerk/tanstack-react-start Patch
@clerk/testing Patch
@clerk/ui Patch
@clerk/vue 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

@vercel
Copy link

vercel bot commented Mar 17, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Mar 18, 2026 1:59pm

Request Review

@nikosdouvlis nikosdouvlis force-pushed the nikos/session-minter-sdk-changes branch from dc3c6e9 to cf6803d Compare March 18, 2026 09:59
@nikosdouvlis nikosdouvlis changed the base branch from nikos/plat-2566-session-minter-sdk-params to nikos/plat-2566-send-token-on-refresh March 18, 2026 13:17
@nikosdouvlis nikosdouvlis force-pushed the nikos/plat-2566-send-token-on-refresh branch from f0b2a14 to cbc83a0 Compare March 18, 2026 13:23
@nikosdouvlis nikosdouvlis changed the base branch from nikos/plat-2566-send-token-on-refresh to main March 18, 2026 13:23
@nikosdouvlis nikosdouvlis changed the base branch from main to nikos/plat-2469-oiat-jwt-header-type March 18, 2026 13:24
Prevent multi-tab race conditions where an edge-minted token with
stale claims overwrites a fresher DB-minted token.

Uses `oiat ?? iat` as the claim freshness metric. A token with oiat
(JWT header) uses oiat as its claim freshness. A token without oiat
is origin-minted (coupled FF), so iat represents claim freshness.

Four guard points:
1. tokenCache handleBroadcastMessage - replaces old iat comparison
2. tokenCache setInternal - async compare-and-swap at resolve time
3. Session #dispatchTokenEvents - before token:update emit
4. AuthCookieService updateSessionCookie - cookie chokepoint with
   session scoping (different sessions always allowed through)

Guard 4 catches the sleeping tab edge case where in-memory guards
pass (stale baseline) but the cookie has a fresher value from
another tab.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant