Cookie Session Authentication
- 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
- sources
- samsch-jwt-auth-gist
- 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.
What the cookie carries
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.
The cookie attributes that matter
HttpOnlyโ JavaScript cannot read the cookie. This neutralizes session theft via XSS.Secureโ only sent over HTTPS. Required in any modern setup.SameSite=Lax(orStrict) โ blocks cross-site CSRF for the common cases.Laxis the modern default;Strictis stronger but breaks following links from external sites while logged in.PathandDomainโ 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=Strictexclusively) - 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.
Related
- 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