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-Cookie: __Host-session=<opaque-token>; Path=/; Secure; HttpOnly; SameSite=Strict; Max-Age=3600
  • HttpOnly blocks document.cookie, which stops session theft through XSS. Secure keeps the cookie off plain HTTP.
  • The __Host- prefix makes the browser require Secure, Path=/, and no Domain, so a subdomain cannot overwrite the cookie (OWASP Session Management Cheat Sheet).
  • SameSite=Strict withholds the cookie on every cross-site request. Lax also sends it on top-level GET navigations, so inbound links land logged in; Chromium applies Lax when the attribute is missing. Use Strict for pure app sessions, Lax when 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) or getUser() (asks the Auth server). Never trust getSession() 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 HttpOnly by default because the browser client reads them, so XSS prevention still matters. If only server code touches auth, set httpOnly in the cookie options.
  • Never ship the secret key (sb_secret_..., legacy service_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.