Overview
Authenticate users with an opaque session token in an HttpOnly, Secure, SameSite cookie, and keep the source of truth on the server so a session can be revoked at once. Rotate the session ID on login, revoke the refresh-token family on reuse, and add CSRF defenses whenever cookies authenticate requests. Opaque sessions versus JWTs are in oauth-vs-jwt.
Set every session cookie with __Host-, HttpOnly, Secure, and SameSite
Set-Cookie: __Host-session=<opaque-token>; Path=/; Secure; HttpOnly; SameSite=Strict; Max-Age=3600
HttpOnlyblocksdocument.cookie, which stops session theft through XSS.Securekeeps the cookie off plain HTTP.- The
__Host-prefix makes the browser requireSecure,Path=/, and noDomain, so a subdomain cannot overwrite the cookie (OWASP Session Management Cheat Sheet). SameSite=Strictwithholds the cookie on every cross-site request.Laxalso sends it on top-level GET navigations, so inbound links land logged in; Chromium appliesLaxwhen the attribute is missing. UseStrictfor pure app sessions,Laxwhen external links must arrive authenticated.- Generate the ID with a CSPRNG. OWASP’s minimum is 64 bits of entropy; 128 bits (16 random bytes) is a common default.
- Bound lifetime with both an idle timeout and an absolute timeout. OWASP’s ranges are 2 to 30 minutes idle and 4 to 8 hours absolute for typical business apps, depending on risk.
Rotate the session ID on login and privilege change
Issue a fresh session ID after each successful authentication and after any privilege change. This defeats session fixation, where an attacker plants a known ID and waits for the victim to log in under it.
Rotate refresh tokens and revoke the family on reuse
Pair a short-lived access token (typically 15 to 30 minutes) with a longer-lived refresh token. On each refresh, issue a new refresh token and invalidate the old one, tracking tokens as a family per session. If an already-rotated token is presented again, treat it as theft: revoke the whole family and force re-authentication. RFC 9700 (OAuth 2.0 Security Best Current Practice, January 2025) codifies rotation with reuse detection. Serialize concurrent refreshes so parallel requests do not trigger false positives.
Add CSRF defenses when cookies authenticate requests
SameSite is defense in depth, not the primary control. Use the framework’s built-in CSRF protection when it has one. OWASP’s order for cookie-authenticated, state-changing requests is otherwise: anti-CSRF tokens (synchronizer token, or signed double-submit cookie for stateless apps), then Sec-Fetch-Site Fetch Metadata checks with an Origin verification fallback, then custom request headers for AJAX endpoints (OWASP CSRF Prevention Cheat Sheet). Validate on every POST, PUT, PATCH, and DELETE. Header-based tokens without a cookie avoid CSRF but expose the token to XSS, which is why HttpOnly cookies stay the default.
Store Supabase sessions in server-managed cookies
With Supabase Auth, use @supabase/ssr so the session lives in cookies the server reads and refreshes, not localStorage. Pass createServerClient a cookies object with getAll() and setAll(cookiesToSet); the older get, set, and remove methods are deprecated.
- On the server, authorize with
getClaims()(validates the JWT signature) orgetUser()(asks the Auth server). Never trustgetSession()for authorization; it reads unverified tokens from storage. - Do not cache responses that set or depend on session cookies at a CDN; a cached response can sign a different user in.
- These cookies are not
HttpOnlyby default because the browser client reads them, so XSS prevention still matters. If only server code touches auth, sethttpOnlyin the cookie options. - Never ship the secret key (
sb_secret_..., legacyservice_role) to a client; it bypasses supabase-rls. See supabase and secrets-and-env.
Make logout revoke the session
Logout must delete the server-side session record, not only clear the cookie; with Supabase, call sign-out so the refresh token is revoked, then clear the cookies. Use the same revocation path for refresh-token reuse, password change, or a reported compromise. Machine callers follow a different model (webhooks); agent credentials are in mcp-security.