# Cookie Session Authentication

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-jwt-auth-gist|samsch gist]] and a long line of similar pieces are pointing at when they say "use sessions instead of [[jwt-for-sessions|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`** (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-for-sessions|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
