Skip to content

fix(auth): close password-reset → login → /me race via cookie-commit sync - #80

Merged
agjs merged 1 commit into
mainfrom
fix/password-reset-race-cookie-commit
Jun 1, 2026
Merged

fix(auth): close password-reset → login → /me race via cookie-commit sync#80
agjs merged 1 commit into
mainfrom
fix/password-reset-race-cookie-commit

Conversation

@agjs

@agjs agjs commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes the password-reset login race that's been .fixme-skipped since May 28. Root cause is a Chromium cookie-commit lag under Playwright; the fix pre-fetches /me with short retries on every session-establishing flow so the post-navigation useMe is a cache hit, not a refetch.

Root cause

The login (or MFA verify / verify-email / OAuth callback) response sets the auth cookie via Set-Cookie, but the browser sometimes hasn't committed the cookie to its jar by the time ProtectedRoute mounts and fires GET /me. /me sees no cookie, returns {user: null}, the SPA treats that as anonymous, and bounces back to /login.

Two earlier partial fixes (ad456b7 JWT iat-lift, the ProtectedRoute isFetching guard) reduced but didn't eliminate the failure. Codex Pass 4 surfaced it again at ~1/3 flake rate under CI-mimicking single-worker runs.

Fix

New helper apps/ui/src/features/auth/Auth.session.sync.tssyncMeAfterSessionEstablished(qc) — calls /me up to 5 times at 30 ms apart (max ~150 ms budget) until it sees an authed user, then returns. The cached me carries staleTime: 60_000, so the value sticks past the navigation.

Called from every session-establishing flow:

  • useLogin (Auth.session.mutations.ts)
  • useMfaVerifyLogin + useMfaVerifyRecovery (Auth.mfa.challenge.mutations.ts)
  • OAuthCallbackPage.hooks.ts
  • VerifyEmailPage.hooks.ts

Why this is durable

Belt-and-suspenders against:

  1. Chromium cookie-commit lag under Playwright (the primary failure mode).
  2. Cache propagation gaps in jwtRevocationService between revokeAllForUser (called during password reset) and buildJWTPayload's getUserRevokeCutoff read on the immediate login. The existing iat-lift covers the common case; this retry budget covers the rare miss.

If /me persistently reports anonymous after 5 attempts, the helper returns null and the consumer proceeds with the navigation — ProtectedRoute will redirect to /login, which is the correct behavior for a genuinely unauthenticated state.

Files

  • New: Auth.session.sync.ts (helper) + Auth.session.sync.test.ts (4 unit tests)
  • Updated mutations/hooks: Auth.session.mutations.ts, Auth.mfa.challenge.mutations.ts, OAuthCallbackPage.hooks.ts, VerifyEmailPage.hooks.ts
  • Test mocks updated in the consumers' tests to stub the follow-up GET /me call
  • password-reset.spec.ts: .fixme removed; comment block records the diagnosis + closure path

Test plan

  • API: 1049/1051 (2 DB-only skipped locally)
  • UI: 577/577
  • Both apps lint + lint-meta + typecheck + knip + format clean
  • Run pre-push-smoke.sh end-to-end against the password-reset spec to verify the fix in the actual browser context

…sync

Root cause: a Chromium cookie-commit lag under Playwright. The login
(or MFA verify / verify-email / OAuth callback) response sets the auth
cookie via `Set-Cookie`, but the browser sometimes hasn't committed
the cookie to its jar by the time ProtectedRoute mounts and fires
GET /me. `/me` sees no cookie, returns `{user: null}`, the SPA treats
that as anonymous, and bounces back to /login.

Fix: every session-establishing flow now pre-fetches /me with short
retries (5 × 30 ms = max ~150 ms) BEFORE the consumer navigates. The
cookie window closes inside the retry budget and the post-navigation
useMe is a cache hit, not a refetch. Same helper used across:

  - `useLogin` (Auth.session.mutations.ts)
  - `useMfaVerifyLogin` + `useMfaVerifyRecovery` (Auth.mfa.challenge.mutations.ts)
  - `OAuthCallbackPage.hooks.ts`
  - `VerifyEmailPage.hooks.ts`

New helper `Auth.session.sync.ts` with sibling unit test covers the
three paths: authed-on-first-call, retry-until-authed, retry-budget-
exhausted. Test mocks updated across the four consumers.

password-reset.spec.ts `.fixme` removed. Commentary in the spec
records the diagnosis + the closure path.

Belt-and-suspenders against:

  1. Chromium cookie-commit lag (the primary failure mode).
  2. Cache propagation gaps in `jwtRevocationService` between
     `revokeAllForUser` (called during password reset) and
     `buildJWTPayload`'s `getUserRevokeCutoff` read on the immediate
     login. The iat-lift in `buildJWTPayload` already covers the
     common case; this retry budget covers the rare miss.

Gates: API 1049/1051 (2 DB-only skipped locally), UI 577/577, both
apps lint + lint-meta + typecheck + knip + format clean.
@agjs
agjs merged commit ad8811d into main Jun 1, 2026
28 checks passed
@agjs
agjs deleted the fix/password-reset-race-cookie-commit branch June 1, 2026 13:38
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