Sessions
Authentication happens once; the session is what makes the next thousand requests work — a random opaque identifier that maps to server-side state you can inspect, expire and revoke.
Frame the problem
Security starts with a concrete asset, attacker capability and trust crossing.
What a session actually is
A session is a server-side record — user id, creation time, last activity, device fingerprint, IP, and whatever else you need — addressed by an opaque random identifier that the client stores and returns. The identifier carries no information; it is a key into a table you control. That indirection is what gives you the properties that matter.
Because the state is yours, you can expire it (absolute and idle timeouts), revoke it (delete the row and the session is gone on the very next request), inspect it (show the user their active sessions with device and location), and change it without asking the client to do anything. None of these are available if the state lives entirely in a self-contained token, which is the central trade in JWT — What It Is and What It Costs.
The identifier itself has exactly two requirements and both are absolute: at least 128 bits of entropy from a cryptographically secure random source, and no derivation from user data. A session id built from a user id and a timestamp, or from a fast hash of predictable inputs, is guessable, and guessable sessions are silent, unlimited impersonation.
Rotation, expiry and the operations that matter
Rotation — issuing a new identifier and discarding the old one — is required at every privilege change. At login above all: if the application issued a session id to the anonymous visitor and simply attaches a user to it after login, an attacker who planted a known id in the victim's browser beforehand now holds an authenticated session. That is session fixation, and generating a fresh id at login removes it entirely. Rotate again on password change, on MFA enrolment, and on any elevation.
Expiry needs two clocks. An idle timeout ends sessions that have stopped being used, which limits the value of a token stolen from a device that has been left alone. An absolute lifetime caps the total duration regardless of activity, which limits the value of a token an attacker keeps warm by making periodic requests. Only the idle timeout is commonly implemented, and the absolute one is what prevents an indefinitely-refreshed stolen session.
Revocation is the operation that turns an incident into something with an end. The user must be able to see their sessions and end them; a password change must end all others by default; and your operations team must be able to revoke in bulk for an account, a tenant, or everyone. Test this path — bulk revocation is exactly the kind of code that is written once, never exercised, and broken when needed.
1const IDLE_MS = 30 * 60_000 // 30 minutes without activity2const ABSOLUTE_MS = 12 * 60 * 60_000 // 12 hours regardless of activity3 4async function createSession(userId: string, ctx: RequestCtx, previousSid?: string) {5 // Fixation defence: never reuse a pre-authentication identifier.6 if (previousSid) await sessions.delete(previousSid)7 8 const sid = crypto.randomBytes(32).toString('base64url') // 256 bits, CSPRNG9 await sessions.put({10 id: sha256(sid), // store the hash: a leaked store must not yield usable sessions11 userId,12 createdAt: Date.now(),13 lastSeenAt: Date.now(),14 ip: ctx.ip,15 userAgent: ctx.userAgent,16 deviceId: ctx.deviceId,17 })18 return sid19}20 21async function loadSession(sid: string) {22 const s = await sessions.get(sha256(sid))23 if (!s) return null24 const now = Date.now()25 if (now - s.lastSeenAt > IDLE_MS) { await sessions.delete(s.id); return null }26 if (now - s.createdAt > ABSOLUTE_MS) { await sessions.delete(s.id); return null }27 await sessions.touch(s.id, now)28 return s29}30 31// Revocation is a delete, and it takes effect on the very next request.32const revokeAllForUser = (userId: string) => sessions.deleteByUser(userId)Where the identifier is allowed to live
A session id in a cookie with the right attributes is the default correct answer for browser applications, because the browser stores it outside JavaScript's reach and attaches it automatically. The attributes are what make that safe, and they are covered in Cookies and Their Attributes.
A session id in a URL is a durable mistake with several independent leak paths: it lands in server access logs, in proxy logs, in browser history, in the Referer header sent to third-party sites, and in any link the user shares. Once it is in any of those, it is a valid credential in a place with no access control.
A session id in `localStorage` is readable by any JavaScript running on the origin, which means any successful XSS — including one introduced by a third-party script you did not write — reads it directly. This is the standard trade made by single-page applications that store tokens client-side, and it should be a conscious decision rather than a default: it exchanges a well-understood CSRF risk (mitigated by SameSite) for a well-understood XSS risk (mitigated by nothing you can retrofit).
The remaining consideration is where the server state lives. A shared store (Redis, a database table) gives every instance the same view and makes revocation immediate; the cost is a lookup per request and a dependency whose failure must be planned for. Sticky sessions with in-memory state avoid the lookup and break every property you wanted: no revocation, no visibility, and sessions lost on deploy.
Key points
- A session id is credential-equivalent for its lifetime; treat it with the same care as a password.
- 128+ bits from a CSPRNG, opaque, carrying no user data. Store its hash server-side, not the value.
- Rotate on login and on every privilege change — that alone eliminates session fixation.
- Two clocks: idle timeout and absolute lifetime. Only the second stops an indefinitely-refreshed stolen session.
- Server-side state is what buys revocation, inspection and bulk termination — and those are what end an incident.
Boundary control exercise
This lesson uses the shared boundary-control exercise.
Follow the attack
Safe conceptual simulation: capability → missing control → crossed boundary → asset impact.
- 1Attacker → obtain a session id: XSS reading `localStorage`, a URL in a log or referrer, a plaintext hop, or a shared machine.
- 2Session id → replay: send it from anywhere; the server sees a valid session and a legitimate user.
- 3Replay → act: everything the user can do, with no credential prompt, for as long as the session lives.
- 4Act → persist: use the session to create a durable credential (API token, added OAuth app) before it expires.
- Complete impersonation for the session lifetime, with no credential to change and no login event to alert on.
- Actions are attributed to the legitimate user in every audit log, which makes disputes and forensics difficult.
- Without server-side revocation the only remedy is waiting for expiry.
Defend, detect, recover
One prevention is a single point of security failure. Layer it and make failure observable.
- • Generate ids with a CSPRNG at 128+ bits and store only their hashes.
- • Rotate on login and privilege change; delete the pre-authentication session.
- • Set both idle and absolute timeouts, shorter for administrative sessions.
- • Keep the id out of URLs entirely, and out of `localStorage` unless the XSS trade has been deliberately accepted.
- • Require step-up authentication for account-takeover primitives so a stolen session cannot become permanent.
- • Alert when one session id is used from two distinct network locations or device fingerprints within a short window.
- • Alert on a session whose user agent or device fingerprint changes mid-life.
- • Alert on sessions that live near the absolute limit with continuous low-volume activity — the keep-alive pattern of a stolen token.
- • Revoke the specific session, then all sessions for the user; the second is usually the right default.
- • Check for persistence created during the session's life.
- • Force re-authentication with a second factor and notify the user out of band.
- • A valid session presented by an attacker is indistinguishable from the user without device or behavioural signals.
- • Session lifetime is a permanent trade between friction and exposure.
- • The session store becomes a high-value target and a dependency whose failure mode must be designed.