feat(auth): move session auth to HttpOnly cookies#5
Merged
Conversation
Stop exposing the auth token to page JavaScript (review finding 1.8). The server now delivers the session token as an HttpOnly, SameSite=Lax cookie (Secure in production) and the SPA keeps no token at all. Server: - Set the session cookie on register/login; clear it on logout. - Read the token from the cookie or the Authorization header (readSessionToken), including on the /ws upgrade — so the WebSocket authenticates from the cookie and the token no longer rides in the ws query string. - Add GET /auth/session so the client can restore a session from the cookie on load instead of reading a token out of localStorage. - Origin-aware CORS: credentialed (cookie) requests from configured origins (CORS_ALLOWED_ORIGINS, default the dev client) get that origin echoed plus Access-Control-Allow-Credentials; everyone else keeps the non-credentialed wildcard. New config: corsAllowedOrigins, cookieSecure. Client: - All authenticated requests use `credentials: "include"` and drop the Authorization header; the token is never held in JS. - Restore the session via GET /auth/session on load; remove all localStorage session storage. logout() clears the server cookie. - NetClient connects without a token in the URL (cookie carries it). - Dedupe the signed-in top-bar visibility into revealSignedInChrome() (finding 8.6), fixing the divergent edit-character button reveal. Verified end-to-end in a real browser (Playwright/Chromium): register sets an HttpOnly cookie with no token in document.cookie or localStorage; reload restores the session and connects the WebSocket from the cookie alone; logout clears it and returns to login. Server cookie/CORS behavior also covered by router tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements review finding 1.8 — the auth token no longer lives in page JavaScript. The server delivers the session as an HttpOnly, SameSite=Lax cookie (Secure in production) and the SPA holds no token at all.
Server
readSessionToken()reads the cookie or anAuthorization: Bearerheader — including on the/wsupgrade, so the WebSocket authenticates from the cookie and the token no longer rides in the ws query string.GET /auth/sessionlets the client restore a session from the cookie on load instead of reading a token fromlocalStorage.CORS_ALLOWED_ORIGINS, default the dev client) get that origin echoed +Access-Control-Allow-Credentials; everyone else keeps the non-credentialed wildcard. New config:corsAllowedOrigins,cookieSecure.Client
credentials: "include"and drops theAuthorizationheader — the token is never in JS.GET /auth/sessionon load; alllocalStoragesession storage removed.logout()clears the server cookie.NetClientconnects without a token in the URL (the cookie carries it).revealSignedInChrome(), fixing the divergent edit-character reveal.Test plan
bun run lint✅ ·bun run typecheck✅ ·bun test✅ 255 pass ·bun run coverage:check✅ 95.2%/auth/session, logout-clear, and origin-aware CORS behavior; client tests updated forcredentials: "include".Set-Cookie … HttpOnly; SameSite=Lax;document.cookieempty,localStorageempty (token unreachable by JS).GET /auth/session → 200restores the session from the cookie alone and connects the WebSocket (connected as user_…).logout → 200, cookie cleared, back to the login form.Notes
Application(and WebGL context) per cell — 28 contexts, past the browser's ~16 limit, so the excess get context-lost. The real app uses a single context per room and renders full-body avatars correctly (verified live). Pre-existing preview-tooling limitation, not in scope here.🤖 Generated with Claude Code