Session Authentication
The server keeps the state and the client carries an opaque handle. Revocation is a delete; the cost is a lookup on every request.
The requirement, the obvious build, and why it breaks
Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind has a problem.
What does a backend actually hold when a user is "logged in" with a session?
A user logs in on the website once and stays logged in across pages, tabs and days, and logging out has to work immediately on every device.
Generate a random id at login, put it in a cookie, keep a map from id to user id, and look it up on each request. Which is, in fact, correct — the failures are all in the details around it.
The cookie is set without HttpOnly, so any script on the page — including one injected through an XSS bug — can read the session id and replay it from anywhere (XSS Defense by Output Context in Security Engineering).
- The cookie is set without
HttpOnly, so any script on the page — including one injected through an XSS bug — can read the session id and replay it from anywhere (XSS Defense by Output Context in Security Engineering). - The cookie is set without
Secure, so a single plain-HTTP request on the domain hands the session to anyone on the network path. - No
SameSitesetting, so a third-party page can cause the browser to send the cookie on a state-changing request the user never intended (Cross-Site Request Forgery (CSRF) in Security Engineering). - The session id is issued before login and reused afterwards, so an attacker who plants a known id can ride it into an authenticated session — session fixation.
- The session never expires, so a token stolen from a device three years ago still works.
- The map is a process-local object, so a second instance cannot see it and every deploy logs everybody out (Stateless Services).
What is actually happening
- The session id is a bearer credential and nothing more. It carries no information; the server holds the mapping from id to subject, and everything the request needs is on the server side (Where Sessions Live).
- It must be unguessable. A cryptographically random value with enough entropy that enumeration is infeasible — not a counter, a UUIDv1, a hash of the user id, or anything derived from data.
- The cookie is a browser mechanism with its own rules.
HttpOnlykeeps it away from scripts,Securekeeps it off plain HTTP,SameSiteconstrains cross-site sending,DomainandPathdecide scope, and expiry decides whether it survives a browser restart (Cookies and Their Attributes in Security Engineering). - Two clocks matter. Idle timeout measures time since last use; absolute timeout measures time since login. Sliding a session forever on activity means a stolen session that is being used never expires.
- Session identity changes on privilege change. The id issued to an anonymous visitor must not be the id that carries an authenticated session; regenerating at login is what closes fixation.
- Logout is a server-side delete, not a cookie clear. Clearing the cookie is a courtesy to the browser; the record is what makes the credential dead.
The login exchange, and the step that closes fixation
Almost every session implementation gets the happy path right. The step that is missed is the small one in the middle: the id the user held before authenticating must not be the id they hold afterwards.
Session fixation exists because a visitor can be given an id by an attacker — through a link, a subdomain, a script — before logging in. If the server keeps that id after authentication, the attacker holds a credential to the victim's account without ever seeing their password.
Every cookie attribute is a decision
SameSite defaults — so treat browser behaviour as a moving floor and set the attributes explicitly rather than relying on any default.A session cookie set with defaults is a session cookie set badly. Each attribute below closes a specific attack or a specific scope problem, and leaving one out is not neutral — it is choosing the permissive option.
The SameSite row is the one that most often needs thought rather than a rule: the correct value depends on whether your application genuinely receives legitimate cross-site top-level navigations that need to be authenticated.
| Attribute | What it prevents | What it costs / when it needs thought |
|---|---|---|
HttpOnly | Script reading the id, so an XSS bug cannot directly steal the session | Nothing for a server-rendered session — this is the one attribute with no real trade-off |
Secure | The cookie ever travelling over plain HTTP | Breaks local development over HTTP unless configured per environment |
SameSite=Lax | The cookie being sent on most cross-site state-changing requests | Cross-site top-level POSTs stop working; some OAuth and payment return flows need care |
SameSite=Strict | More of the same, including top-level navigations | A user following a link from another site arrives logged out, which reads as a bug |
SameSite=None | Nothing — it opts out, and requires Secure | Only for genuinely cross-site embedding; you now need CSRF defence to carry the whole load |
Domain narrow | A subdomain you do not control receiving the session | A shared cookie across subdomains has to be a deliberate decision with a threat model |
Max-Age / Expires | A cookie surviving indefinitely on the device | A session cookie with no expiry dies with the browser, which some users find surprising |
Path | Sending the cookie to unrelated parts of the origin | Weak isolation — same-origin scripts are not constrained by path |
Lifetime is two clocks, not one
Session lifetime is usually implemented as one number and needs to be two. Idle timeout limits how long an abandoned session stays usable; absolute lifetime limits how long any session — including one being actively abused — can live at all.
The right values are entirely application-dependent, and this is a case where copying someone else's numbers is genuinely wrong: an internal admin console, a consumer app and a financial dashboard have different tolerance for both risk and re-authentication friction.
1interface SessionRecord {2 id: string // stored hashed; never logged3 subject: string4 issuedAt: number // absolute clock starts here5 lastSeenAt: number // idle clock6 authMethod: 'password' | 'password+mfa' | 'sso'7}8 9function resolve(rec: SessionRecord | null, now: number, p: Policy) {10 if (!rec) return null11 if (now - rec.issuedAt > p.absoluteMaxMs) return null // hard cap12 if (now - rec.lastSeenAt > p.idleMaxMs) return null // abandoned13 // slide the idle clock only; never the absolute one14 store.touch(rec.id, now)15 return principalFrom(rec)16}Sliding lastSeenAt is what keeps active users logged in. Never sliding issuedAt is what guarantees that a stolen session eventually dies even while it is being used. authMethod is recorded so a later step-up requirement can ask how this session was established.
How to build it
Most important first.
- Generate the id from a cryptographic random source, with enough bytes that guessing is not a strategy. Let a maintained session library do it.
- Set the cookie attributes deliberately, every time:
HttpOnly,Secure, an explicitSameSite, the narrowestDomainandPaththat work, and an explicit expiry. - Regenerate the session id on login, on privilege elevation, and on any step-up authentication — carrying the session data across, discarding the old id.
- Enforce both an idle timeout and an absolute maximum lifetime, and pick both from what the application actually is: a banking console and a photo gallery have different right answers.
- Store what the request needs and nothing more — subject id, tenant, issued-at, authentication method. Sessions are not a cache and not a scratchpad (Where Sessions Live).
- Delete server-side on logout, on password change, and on any event that should end every device's access. Provide a "log out everywhere" path, which is trivial when sessions are records and hard when they are not.
- Pair sessions with CSRF defence, because a cookie is attached by the browser automatically and that is the whole basis of the attack (CSRF Defense in Security Engineering).
- Record the session id's existence, never its value, in logs.
What can go wrong
- A cookie scoped to a parent domain, shared with a subdomain you do not fully control, so a compromise there is a compromise everywhere.
- Sessions treated as a general key-value store — a shopping cart, a wizard's state, a large object graph — until the store becomes a load-bearing database with no schema.
- Sliding expiry with no absolute cap, so an actively abused session lives forever.
- Logout that clears the cookie and leaves the record, so a copy of the id taken earlier still authenticates.
- The session store becoming a hard dependency of every request with no degraded mode, so its outage is a total outage (Where Sessions Live).
- Sticky sessions used to paper over process-local storage, which works until an instance is replaced and takes its users' sessions with it (Sticky Sessions).
- Session fixation is a race in structure: the attacker plants an id, the victim authenticates, and whether the attacker wins depends entirely on whether the id was regenerated at that moment.
- Concurrent requests updating session data — sliding expiry, a last-seen field, a wizard step — read-modify-write the same record, and one update overwrites the other unless the store supports atomic field updates (Atomic Operations).
- Logout racing an in-flight request: the record is deleted while a request that already read it continues. Nothing prevents that window; it is bounded by request duration, which is the argument for short handler timeouts on sensitive operations (Timeouts).
- The session id is equivalent to the password for as long as it lives. It must be treated as a secret in transit, in storage and in logs (Session Hijacking in Security Engineering has the attacker's view).
- Regenerate on login or accept session fixation. This is the single most commonly missed step in an otherwise correct implementation.
- Cookie-borne credentials are sent automatically by the browser, which is what makes CSRF possible and why
SameSiteplus a token or origin check is not optional for state-changing requests. - Bind the session to as much context as you can tolerate — but be careful: binding to IP breaks mobile users who change networks, and binding to user agent breaks on browser updates. Both are useful signals for detection, poor ones for hard enforcement.
- A password change or a reported compromise must invalidate every session for that subject, which is exactly the operation self-contained tokens cannot do immediately (Token Authentication and the Revocation Problem).
- Store session records with the same care as any other user data; a dumped session table is a set of live credentials unless the ids are stored hashed.
- "Sessions do not scale." A session lookup is a key-value read; systems with enormous traffic run on sessions. What does not scale is keeping them in one process's memory (Where Sessions Live).
- "Tokens replaced sessions." They moved the trade-off. You give up immediate revocation to remove the lookup (Token Authentication and the Revocation Problem).
- "Logging out clears the cookie, so the session is gone." Only the browser's copy. Anyone holding the value still has a credential until the record is deleted.
- "
SameSitesolves CSRF." It substantially reduces it and it is not a complete defence across all browsers, request types and configurations (CSRF Defense in Security Engineering). - "A UUID is random enough." Some UUID versions are time- and MAC-derived and are not credentials. Use a cryptographic random generator explicitly.
Operating it
- Active session count, and sessions created per minute. A spike in creation with no matching login spike means something is looping.
- Session-store latency and error rate as a first-class dependency metric — it is on the path of every authenticated request.
- Count session lookups that miss. A rising miss rate means expiry, eviction or an instance that cannot see the store.
- Count id regenerations at login; a drop to zero after a refactor is how you find that fixation protection was removed.
- Log the subject and a session fingerprint — a hash prefix, never the id — so a support report can be traced to a session without the log becoming a credential store.
- The lookup per request is the defining cost. At 10x it is 10x load on the session store, and at 100x the store's latency is a floor under every authenticated request (Where Sessions Live).
- Multiple instances make process-local sessions a bug rather than a shortcut; this is the concrete case behind Making an Existing Service Stateless.
- Very long sessions plus many devices mean the number of live sessions grows faster than the number of users, which is a storage and expiry-policy question rather than a code one.
- Server-held state gives you immediate revocation and costs a lookup on the hot path and a store you must keep available.
- Short timeouts reduce the value of a stolen session and log real users out, which pushes them toward "remember me" — a longer-lived credential with its own risks.
- Cookies give you
HttpOnly, which no header-based scheme can offer, and they bring CSRF, which header-based schemes largely avoid. That trade is the actual difference, not "cookies are old".
Where this applies
Backend advice is context-sensitive. These labels say what each claim is specific to, and where a different stack or scale would differ.
- GENERALOpaque handle plus server-held state is a stack-independent design; the id, the store and the lifetime rules look the same everywhere.
- FRAMEWORK-SPECIFICDefaults differ in ways that decide whether you are safe out of the box: Django and Rails set
HttpOnlyby default and expose an explicit rotate-on-login, Express withexpress-sessionrequires you to setcookie.secure,cookie.sameSiteand to callregenerate()yourself, and PHP needs an explicitsession_regenerate_id(). The same code review question has a different answer per framework. - PROTOCOL-SPECIFICCookie behaviour is an HTTP-and-browser mechanism. A native mobile client or a service caller has no cookie jar and no
SameSite, so the same session model there means putting the id in a header and losing bothHttpOnlyand the automatic-send behaviour that CSRF depends on.
Where the depth lives
This domain teaches the application-side mechanism and hands the rest off.