Sessions and authorization

Authenticate every request

Resolve a session cookie into trusted identity context and reject missing, expired, revoked, or malformed sessions consistently.

HTTP has no memory. Every request to the Books API arrives on its own, and the server works out who is calling from scratch each time. That job belongs in one place: an authentication middleware that runs before any handler.

What the middleware does

The steps are always the same:

  1. Read the session cookie.
  2. Check the value has the expected encoding and length. Reject anything odd early.
  3. Hash it, if you store hashed identifiers.
  4. Load the session record from the store.
  5. Check expiry and revocation.
  6. Attach a small identity context to the request.

Authority comes from the server record, never from the cookie itself. The cookie is an opaque string: don’t parse it, don’t trust anything it seems to say. And never put session tokens in URLs or logs, because both get copied around.

Keep the context small

The middleware output should be tiny:

type AuthContext = {
  userId: string
  sessionId: string
  authenticatedAt: Date
}

Notice what’s missing. No roles, no email, no “is admin” flag. Authorization data changes. If you copy the role into the session at login, a demoted administrator keeps admin power until they log out. Load roles where you make the decision, or store a version number in the session that forces stale ones to refresh.

A missing session is not an outage

Two situations look similar and must be handled differently.

No cookie, or a cookie that matches nothing: the caller is anonymous. Answer 401 Unauthorized.

The session store is down: you don’t know who the caller is. That’s a server failure, 503. Be careful with routes that serve both public and private results, like a book list that shows public books to anyone and private ones to the owner. If an outage silently turns everyone into “anonymous”, you’ve hidden an incident. Fail closed and say so.

Run it first

Authentication runs before the handler reads the body, parses uploads, or starts any expensive work. The handler receives a trusted AuthContext or it doesn’t run at all. No if (req.user) halfway through.

Watch the caches

A shared HTTP cache, a CDN or a reverse proxy, must never serve one user’s private response to another. Mark private responses with Cache-Control: private, no-store. If you cache inside the application, put the user ID in the cache key.

Try this: add the middleware to the Books API and protect the create, update, delete, and private list routes. Then make a request as a logged-in user and check it succeeds. Revoke that session directly in the database and repeat the request with the same cookie. It must return 401, and a log line inside the handler must not appear. If it appears, the handler ran before the check.

Lesson completed