Overview
Authenticate users with an opaque session token delivered in an HttpOnly, Secure, SameSite cookie, and keep the source of truth server-side. The browser should never read the token from JavaScript, the cookie should never travel over plain HTTP, and the server should be able to revoke a session immediately. The choice of session format (opaque server session vs JWT) is its own decision; see oauth-vs-jwt for when each wins. This page is the house standard for the cookie flags, rotation, and CSRF defenses that apply regardless of format.
Set every session cookie with HttpOnly, Secure, and SameSite
HttpOnly blocks document.cookie access, which is mandatory to stop session theft via XSS. Secure keeps the cookie off unencrypted HTTP. SameSite controls cross-site sending: Strict is the safest and blocks the cookie on all cross-site requests; Lax (the browser default) allows it on top-level GET navigations, which keeps inbound links working. Prefer Strict for pure app sessions; use Lax when external links must land logged in. Scope with Path=/ and bound the lifetime with Max-Age.
Set-Cookie: __Host-session=<opaque-token>; Path=/; Secure; HttpOnly; SameSite=Strict; Max-Age=3600
The __Host- prefix forces Secure, Path=/, and no Domain, which hardens the cookie against subdomain injection (OWASP Session Management Cheat Sheet). Use a session ID of at least 128 bits from a CSPRNG.
Rotate the session ID on every login
Issue a fresh session ID after each successful authentication and after any privilege change. This defeats session fixation, where an attacker plants a known session ID and waits for the victim to log in under it. OWASP requires regenerating the ID on login and recommends periodic rotation during long sessions.
Rotate refresh tokens and revoke the family on reuse
Pair a short-lived access token (commonly 15 to 30 minutes) with a longer-lived refresh token. On each refresh, issue a new refresh token and invalidate the old one (single-use rotation). Track tokens as a family per session. If a refresh token is presented after it was already rotated, treat that replay as a compromise: revoke the entire family, invalidate active access tokens, and force re-authentication. RFC 9700 (OAuth 2.0 Security Best Current Practice, January 2025) codifies rotation and reuse detection. Serialize concurrent refreshes so parallel requests do not trigger false-positive reuse.
Store Supabase tokens in HttpOnly cookies, never localStorage
With Supabase Auth, do server-side session handling: keep the access and refresh tokens in HttpOnly cookies set by your server (the @supabase/ssr helpers do this), not in localStorage where XSS can read them. Supabase rotates refresh tokens on use. Never ship the service_role key to the browser or any client bundle; it bypasses Row Level Security. Keep service_role server-side only (see secrets-and-env) and enforce per-user access with supabase-rls. TODO: verify exact @supabase/ssr cookie option names against current Supabase docs. See supabase for project setup.
Add CSRF defenses when cookies authenticate requests
SameSite cookies reduce CSRF exposure but are not a complete defense; older browsers and some navigation paths weaken it. For state-changing requests authenticated by cookies, add an anti-CSRF token. Use the synchronizer token pattern or a double-submit cookie, and validate it on every POST, PUT, PATCH, and DELETE (OWASP CSRF Prevention Cheat Sheet). Token-in-header auth without a cookie sidesteps CSRF but reintroduces XSS token-theft risk, which is why HttpOnly cookies remain the house default.
Make logout actually revoke the session
Logout must delete the server-side session record, not just clear the cookie. With opaque sessions, drop the row or cache entry so the token is dead immediately. With Supabase, call sign-out so the refresh token is revoked server-side and clear the cookies. Apply the same revocation path when you detect refresh-token reuse, a password change, or a reported compromise. Webhook and machine callers follow a separate model; see webhooks. Agent and MCP credential handling is covered in mcp-security.
Related
- oauth-vs-jwt - opaque sessions vs JWTs and why not to use JWT as a session store
- supabase - Supabase project and auth setup
- supabase-rls - per-user access control that
service_rolebypasses - secrets-and-env - keeping
service_roleand signing keys server-side - python-security - server-side input and credential handling
- webhooks - authenticating machine callers without browser cookies
- mcp-security - credential handling for agents and MCP servers