Skip to content

fix(auth): stop random sign-outs with several tabs open (token mode) - #31

Merged
vlidholt merged 1 commit into
serverpod:mainfrom
vlidholt:fix/shared-web-session
Oct 21, 2026
Merged

vlidholt merged 1 commit into
serverpod:mainfrom
vlidholt:fix/shared-web-session

Conversation

@vlidholt

Copy link
Copy Markdown
Contributor

Problem

Users on wonky.work get signed out at random. Production runs Serverpod's token mode ("cookieAuth": false: Serverpod Cloud has no authCookie), where every tab reads the refresh token from shared browser storage and refreshes with no coordination between tabs. Refresh tokens are single-use, and the server revokes the session when a rotated one comes back. So two tabs refreshing at the same moment (laptop wake, tabs reloading together, tabs sharing the same expiry) sign the user out everywhere. Every page load and app switch also forced a rotation (ClientAuthSessionManager.initialize()), which made this more likely.

Fix (client only, no Serverpod or server code changes)

On web in token mode, the apps replace Serverpod's JWT key provider (SharedJwtAuthKeyProvider, packages/wonky_app/lib/src/auth/shared_session.dart):

  • One shared session: stored in localStorage (wonky.auth.session) and read fresh on every use. Other tabs pick up sign-in, sign-out and rotations through storage events. Existing sessions are moved once from the old flutter_secure_storage, so nobody is signed out when this deploys.
  • One refresh at a time: a refresh runs under the cross-tab Web Lock Serverpod already uses in cookie mode and re-reads the session inside it. When another tab rotated meanwhile, its token is used instead of rotating again.
  • No rotation on page load: startup checks the stored token with status.isSignedIn(). It rotates only when the token is about to expire, or once if the server rejects it.
  • No lost tokens on app switches: switching apps and reloading wait (up to 5 s) for a refresh in flight.

Desktop and cookie mode are unchanged.

Config

  • development.yaml: authCookie and allowedOrigins are removed, so local development matches production.
  • Bundled config.json: set to cookieAuth: false in every app.
  • Deleted files: config/staging.yaml and config/production.yaml were never used (Serverpod Cloud generates the config from its variables).
  • Docs: specs/01 §2.5 and §6, specs/13 §4 and the board's GitHub README are updated.

Testing

  • Unit tests: 12 new tests in packages/wonky_app/test/auth/shared_session_test.dart. The key one simulates two tabs refreshing at once against a fake server that revokes reused tokens like Serverpod does. It fails with Serverpod's stock refresh; the new code rotates exactly once.
  • Existing tests: the wonky_app auth, bootstrap, shell and settings tests pass, as do the server config tests. dart analyze is clean.
  • Manual check: local release builds of Drive and Write in token mode, in the browser:
    • a reload and a Drive → Write switch keep the session without rotating;
    • two tabs reloaded at the same instant with an expiring token make one refresh call in total and both stay signed in;
    • signing out in one tab shows "Your session expired" in the other right away.

Not covered

  • Lost refresh responses: a response lost to sleep, a network drop or a deploy can still sign the user out. Fixing that needs a grace period for the previous secret in Serverpod's rotation (JwtAdmin.rotateRefreshToken), which should go upstream.
  • Debug apps in development: the flutter run apps on ports 8100–8104 now need a sign-in each, because localStorage is per origin.

🤖 Generated with Claude Code

Production runs Serverpod's token mode (no authCookie on Serverpod
Cloud): every tab read the refresh token from shared storage and
refreshed without coordination, so two tabs refreshing at once (laptop
wake, tabs reloading together) reused a rotated refresh token and the
server revoked the session. Every page load and app switch also forced
a rotation.

The apps now replace the JWT key provider on web in token mode:
- the session lives in localStorage (wonky.auth.session), read fresh on
  every use and synced to other tabs through storage events; it is moved
  once from the old flutter_secure_storage so nobody is signed out;
- a refresh runs under the cross-tab Web Lock and re-reads the session
  inside it, using another tab's fresh token instead of rotating again;
- a page load checks the stored token instead of forcing a rotation;
- switching apps and reloading wait for a refresh in flight.

Development now matches production: authCookie and allowedOrigins are
removed from development.yaml, the bundled config.json files set
cookieAuth false, and the unused staging.yaml and production.yaml are
deleted (Serverpod Cloud generates the config from its variables).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vlidholt
vlidholt merged commit a7c805a into serverpod:main Oct 21, 2026
41 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 21, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant