Sessions and tokens

Keep somebody logged in without leaving a credential lying around that never expires.

07of 16
≈ 12Min read
6Parts

What a session has to do

  • Identify the user on every request, without asking for the password again.
  • Expire — after inactivity, and absolutely, whatever happens.
  • Rotate at every change of privilege, especially at login.
  • Be revocable — a logout, a password change, a stolen laptop.
  • Be unguessable — at least 128 bits from a cryptographic random source, never a counter, never a hash of the email.

Session fixation

An attacker sets a session id in the victim's browser before they log in — through a link with a session parameter, a subdomain they control, or an XSS. If the id survives the login, the attacker's session is now an authenticated one.

Rotating at login costs one line. Rotate again at any elevation: becoming an admin, entering a payment flow, confirming a second factor.

the bug is what is missing
// wrong: the id the visitor arrived with is still the id after login
req.session.userId = user.id;

// right: a brand-new session, and the old one destroyed
await req.session.regenerate();
req.session.userId = user.id;

Two clocks

Idle timeout protects the shared computer. Absolute timeout bounds how long a stolen session is useful, and stops a session living for months because a tab keeps polling. Pick the numbers from what the account can do: a banking session is minutes, a blog reader is weeks.

idle and absolute, and both are needed
const IDLE = 30 * 60 * 1000;          // 30 minutes without a request
const ABSOLUTE = 12 * 60 * 60 * 1000; // 12 hours, however active you are

const valid = (session, now) =>
  now - session.lastSeen < IDLE &&
  now - session.createdAt < ABSOLUTE;

Server-side sessions, and why revocation is the deciding factor

The industry spent a decade putting JWTs in cookies to avoid a lookup that a Redis hit answers in under a millisecond, and then re-invented revocation with deny lists. For an ordinary web application, a session id in an HttpOnly cookie with a server-side store is the simpler and stronger default.

Session id + server storeSelf-contained token (JWT)
Lookup per requestyes — a cache hitnone
Revoke nowdelete the rownot possible without a deny list
Change a role immediatelyyesonly at the next refresh
Size~32 byteshundreds of bytes, on every request
Good fora normal web applicationservice-to-service, short-lived access tokens

If you do use tokens

Short-lived access token (minutes) plus a long-lived refresh token in an HttpOnly, path-scoped cookie — rotated on every use, so a stolen refresh token is detectable when both copies are used. That is the pattern worth copying; everything simpler than it is a password that lasts a month.

the mistakes that keep recurring
// 1. trusting the header
const { alg } = JSON.parse(atob(token.split(".")[0]));   // "none" — and libraries used to accept it
// fix: pin the algorithm yourself, and never read it from the token

// 2. not checking exp — a token that never expires is a password
// 3. keeping it in localStorage — see the storage lesson
// 4. putting anything secret in the payload: it is base64, not encryption
// 5. a 30-day access token "so users do not get logged out"
everything from the web course, in one line
Set-Cookie: sid=<128 random bits>; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=43200

Practice