Map

Cookie Session Authentication

Wiki conceptauthenticationsecurityweb โ†ณ show in map Markdown
title
Cookie Session Authentication
type
concept
summary
The default browser-session pattern: opaque random ID in an HttpOnly cookie, server-side store maps it to user state
tags
authentication, security, web
created
2026-04-25
updated
2026-04-25

The standard pattern for keeping browser users logged in. After successful login, the server generates a long random opaque session identifier, stores it in a server-side data store keyed against the user and any per-session metadata (CSRF token, last-active timestamp, IP, user-agent, role), and returns it to the browser as a Set-Cookie header. The browser sends the cookie on every subsequent request to the same origin; the server looks up the session record on each hit.

This is what the samsch gist and a long line of similar pieces are pointing at when they say "use sessions instead of JWTs." It's not new, it's not exciting, and it works.

The cookie value is just the session ID โ€” an opaque random string with enough entropy that it cannot be guessed (typically 128+ bits, base64-encoded). It does not carry any user data. All the user data lives server-side under that key, which is why revocation, rotation, and updates are trivial: change the row, the next request reflects the change.

  • HttpOnly โ€” JavaScript cannot read the cookie. This neutralizes session theft via XSS.
  • Secure โ€” only sent over HTTPS. Required in any modern setup.
  • SameSite=Lax (or Strict) โ€” blocks cross-site CSRF for the common cases. Lax is the modern default; Strict is stronger but breaks following links from external sites while logged in.
  • Path and Domain โ€” scope the cookie to where it's needed. Default to the most restrictive that still works.
  • Max-Age / Expires โ€” controls the cookie lifetime in the browser. The server should also enforce its own session expiration independently.

What lives in the server-side session store

  • User ID and role/permissions snapshot (refresh on permission changes if you don't want to look it up per-request)
  • Created-at, last-seen-at, expires-at
  • CSRF token (if not using SameSite=Strict exclusively)
  • Whatever per-session state the app needs (active workspace, feature flags, MFA-passed flag)

Storage backends are mundane: Postgres or MySQL table, Redis for fast eviction, SQLite for small apps. Most web frameworks ship a session middleware that wraps this. For Express the gist recommends express-session + connect-session-knex. Django, Rails, Laravel, ASP.NET, and Phoenix all ship sessions out of the box.

Revocation, rotation, and forced logout

All of these are one query. To kick a user, delete their session row. To force logout on password change, delete all their session rows. To rotate the session ID on privilege escalation, generate a new ID, copy the row, and clear the old. None of this requires touching the cookie format or signing keys.

This is the property jwt-for-sessions keeps trying to recreate with refresh tokens, denylists, and rotating signing keys โ€” at which point it has reinvented sessions, with extra steps.

CSRF protection

SameSite=Lax blocks the most common CSRF vectors but is not a complete defense (especially on top-level GET requests that mutate state, which you shouldn't have anyway). The traditional pattern is a per-session CSRF token that the server includes in forms / responds with on first GET, and that the client echoes back on every state-changing request. Frameworks usually wire this for you.

SPA and mobile

Cookie sessions work for SPAs. The browser handles cookies for fetch (with credentials: 'include') and for navigations the same way. CORS configuration needs to allow credentials on cross-origin calls, and SameSite must be picked accordingly.

For native mobile, many apps use Bearer tokens โ€” that's a reasonable place for a short-lived signed token (and where paseto would be more appropriate than JWT), with refresh against the auth server. The browser-session arguments don't fully apply.

  • jwt-for-sessions โ€” the anti-pattern this concept is the alternative to
  • paseto โ€” better-designed short-lived-token spec for the rare cases that need one
  • samsch-jwt-auth-gist โ€” the short-form case for picking sessions over JWTs