Skip to content

fix: clear invalid session cookie on login render#2454

Merged
aalemayhu merged 2 commits into
mainfrom
fix/login-loop-clear-invalid-cookie
May 19, 2026
Merged

fix: clear invalid session cookie on login render#2454
aalemayhu merged 2 commits into
mainfrom
fix/login-loop-clear-invalid-cookie

Conversation

@aalemayhu
Copy link
Copy Markdown
Contributor

@aalemayhu aalemayhu commented May 19, 2026

What

When a user visits /login with a stale or invalid session cookie, LoginPage now clears that cookie on render. Previously the stale cookie persisted in the browser, causing the server-side checkUser to see a cookie and redirect the user back to /upload on the next hard navigation — creating a loop.

Why

Fixes #2352. Part of wave 2 of 4 in the first-deck-success activation wave (umbrella PR #2446).

The root cause: checkUser in UsersController.ts calls getUserFrom(req.cookies.token). With a valid token it redirects to /upload (or wherever). With no cookie it serves index.html (the login form). With a stale cookie (expired JWT, rotated SECRET), getUserFrom throws and Express 5 returns a 500. The stale cookie persists across browser sessions until it expires or the user manually clears it — but during that window the user can land in a redirect loop.

The fix is symptom-level and intentional per the spec: it prevents the loop regardless of why the cookie is invalid. Root causes (SECRET rotation, OAuth client rotation) remain separate issues.

How

LoginPage.tsx adds a useEffect that watches the token cookie and the isError flag from useUserLocals. When both are true (cookie present, fetch errored), it removes the cookie via useCookies. The fix does not affect the login flow, the useHandleLoginSubmit hook, or server-side code.

The cookie was set without HttpOnly (plain res.cookie('token', token) in UsersControllers.ts), so it is readable and deletable from JavaScript.

Measuring success

After this ships: a user with a stale cookie who navigates to /login will see the login form on first render without a redirect loop. Measurable via Sentry (no more 500 cascade on /login from stale cookies) and by direct testing with an expired JWT.

Testing

  • Unit tests added: web/src/pages/LoginPage/LoginPage.test.tsx — 4 tests covering:
    • No-cookie path renders form normally
    • Valid cookie + user data renders form normally
    • Stale cookie + isError from useUserLocals → cookie removed
    • No cookie + isError → no change (nothing to remove)
  • All 19 LoginPage tests pass (4 new + 15 existing)
  • Full web vitest suite: 746 tests pass across 96 files
  • TypeScript: clean
  • Biome lint: clean

Changelog

Deferred to the wave's final PR per the first-deck-success spec (umbrella PR #2446). A single combined entry will land there.

Risks

  • The useEffect fires on every render where cookie+isError are true, but since removeCookie is stable and React deduplicates effects, this is safe.
  • useUserLocals uses retry: 3 — the effect fires after the third failure settles (isError becomes true), not on each retry attempt.
  • Does not change the server-side checkUser or getUserFrom behavior. The root loop causes (SECRET rotation, etc.) are tracked separately.

Goal alignment

Removes a friction point that makes the site look broken to returning users with stale cookies — directly addresses onboarding/re-activation conversion. Moves toward the 300K-user goal by not losing users at the login door.

Sonar

sonar-scanner not run locally — SONAR_TOKEN not configured in this environment. Changes are small (1 useEffect, 1 test file). No new HTTP calls, no user-input reflection, no zip extraction, no taint paths.


View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

When a user visits /login with a stale or rotated-secret cookie the
useUserLocals query errors. Previously the stale cookie stayed in the
browser, causing the server-side checkUser to redirect back to /upload
on the next hard navigation (seeing a cookie → assumes logged in).

LoginPage now removes the token cookie in a useEffect whenever the
userLocals fetch errors and a cookie is present. This breaks the loop
regardless of why the cookie is invalid (expiry, SECRET rotation, etc.).

Fixes #2352. Part of wave 2/4 of the first-deck-success activation wave
(umbrella PR #2446).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@netlify
Copy link
Copy Markdown

netlify Bot commented May 19, 2026

Deploy Preview for notion2anki ready!

Name Link
🔨 Latest commit 661508c
🔍 Latest deploy log https://app.netlify.com/projects/notion2anki/deploys/6a0c71d1ebce7a000926e224
😎 Deploy Preview https://deploy-preview-2454--notion2anki.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

The LoginPage.tsx fix (removeCookie on isError) required useUserLocals to
surface a genuine error state. The api.ts get() function was returning
undefined on 401 (after a no-op redirectToLogin on /login), which kept
isError false and prevented cookie clearance.

Fix api.ts to throw on 401 so TanStack Query sets isError: true after
retries exhaust. Extend mock-server to return 401 when token=stale-jwt-token.
Add Playwright test that sets the stale cookie, navigates to /login, and
asserts the form renders and the cookie is cleared.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@aalemayhu aalemayhu merged commit 2ea0476 into main May 19, 2026
9 checks passed
@aalemayhu aalemayhu deleted the fix/login-loop-clear-invalid-cookie branch May 19, 2026 14:28
@sonarqubecloud
Copy link
Copy Markdown

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.

Login page should terminate redirect loops on invalid session cookie

1 participant