Session management: cookies, tokens, timeouts, and revocation

How web sessions are built and defended: cookie attributes (HttpOnly, Secure, SameSite, Domain, Path), server-side session tokens versus JWTs and their trade-offs, sliding versus absolute timeouts, idle timeout and forced re-authentication, how to actually revoke a session, and the controls against CSRF, XSS, and session hijacking.

The session is where authentication lives on

Authentication is a moment — the instant the user proves who they are. But HTTP is stateless: without help, the server would forget that proof on the very next request and demand the password again for every click. The session is the mechanism that remembers “this browser already authenticated,” so the user proves themselves once and stays recognized.


That makes the session one of the most attacked assets in any application. A valid session ID is a bearer of authenticated identity: whoever holds it is the user, no password needed. The SSO article kept bumping into this — the master IdP session, the independent app sessions, why logout is hard. This article opens that box: how sessions are built out of cookies and tokens, how long they should live, how to end them, and how to defend the layer against the attacks that target it.


This is a technical article; expect concrete attributes, headers, and trade-offs. The canonical field reference is the OWASP Session Management Cheat Sheet, and it pairs well with everything below.


Cookies: the container and its attributes

For browser apps, the session almost always rides in a cookie — a small value the server sets and the browser returns automatically on subsequent requests. The value itself should be an opaque, high-entropy identifier. The security is mostly in the attributes you set alongside it. Miss one and you hand an attacker a lever.

Set-Cookie: sid=9f2a...e71; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=1800
AttributeWhat it doesWhy it matters
HttpOnlyHides the cookie from JavaScript (document.cookie)An XSS payload cannot read the session ID — the single most important flag
SecureSends the cookie only over HTTPSPrevents interception of the session ID in cleartext
SameSiteControls attachment to cross-site requestsThe primary browser-level defense against CSRF
DomainWhich hosts receive the cookieToo broad a domain leaks the cookie to subdomains that shouldn’t have it
PathWhich URL paths receive the cookieScopes the cookie; a minor, defense-in-depth control
Max-Age / ExpiresCookie lifetimeOmit for a session cookie that dies with the browser; set for persistence

SameSite in detail

SameSite is subtle enough to deserve its own note because it is your front-line CSRF control:


  • Strict — the cookie is never sent on any cross-site request, even top-level navigation. Strongest, but it means following a link from an email or another site lands the user logged-out, which can hurt UX.
  • Lax — the cookie is sent on top-level GET navigations (clicking a link) but not on cross-site POSTs, iframes, or background requests. The pragmatic default for session cookies: it blocks the classic CSRF vector while keeping normal navigation working.
  • None — the cookie is sent on all cross-site requests and must be paired with Secure. Only use it when you genuinely need cross-site cookies (some embedded or federated flows), and compensate with anti-CSRF tokens.


Server-side session tokens vs JWTs

Behind the cookie sits a choice that shapes everything about lifetime and revocation: is the session stateful (server-side) or stateless (self-contained)?


Server-side session tokens (opaque)

The cookie holds a random opaque ID that means nothing on its own; it is a key into session state the server stores (in memory, Redis, a database). On each request the server looks up the ID and reads the associated identity, roles, and metadata.


  • Pros: the server is the source of truth. You can revoke instantly by deleting the record. You can store rich, changing state. The cookie leaks no information if intercepted (it is just a random string).
  • Cons: requires a server-side store and a lookup on every request; that store must scale and be shared across instances.

JWTs as sessions (self-contained)

A JWT is a signed, self-describing token carrying the claims (subject, roles, expiry) inside it. The server validates the signature and reads the claims without a lookup — nothing is stored server-side.


  • Pros: stateless and horizontally scalable; no session store; convenient across services and APIs (it dovetails with the OAuth/OIDC world from the protocols module).
  • Cons: hard to revoke. Because the server stores nothing, there is nothing to delete — a stolen JWT is valid until it expires. Claims can also go stale (a role revoked mid-session still shows in the token). And a larger token travels on every request.

Server-side tokenJWT
Source of truthServer storeThe token itself
RevocationInstant (delete record)Hard (wait for expiry / denylist)
ScaleNeeds shared storeStateless, easy
State freshnessAlways currentCan be stale until reissue
Best fitSensitive web apps needing controlAPIs/microservices needing scale


Lifetime: idle vs absolute timeouts

A session should not live forever. Two independent clocks govern how long it lasts, and mature systems run both.

stateDiagram-v2
  accTitle: Web session lifecycle with idle and absolute timeouts
  accDescr: A session starts anonymous. Successful login moves it to active. From active, inactivity moves it toward idle; any request returns it to active and resets the idle clock. Reaching the idle timeout or the absolute timeout moves the session to ended. Logout or administrative revocation also moves it to ended from active. From ended, re-authentication returns the user to active with a new session.
  [*] --> Anonymous
  Anonymous --> Active: login (authenticate)
  Active --> Active: request resets idle clock
  Active --> Idle: inactivity
  Idle --> Active: request before idle timeout
  Idle --> Ended: idle timeout reached
  Active --> Ended: absolute timeout reached
  Active --> Ended: logout / revoked
  Ended --> Active: re-authenticate (new session)
  Ended --> [*]
The session lifecycle. Login moves the user from anonymous to active. Inactivity slides toward the idle-timeout edge; any request resets it back to active. Two things end the session: reaching the idle timeout, or hitting the absolute timeout that caps total lifetime from login. Logout or an administrative action can revoke it at any point. An ended session forces re-authentication to start a fresh one.

  • Idle (sliding) timeout — ends the session after a period of inactivity, and resets on every request. An actively working user is never interrupted; a session left open on an unattended machine dies on its own. Typical values run from a few minutes for sensitive apps to tens of minutes for ordinary ones.
  • Absolute timeout — ends the session a fixed time after login, no matter how active the user is. It caps total lifetime and forces periodic re-authentication, so even a continuously used (or continuously stolen) session cannot live indefinitely. Typical values run hours for web apps, shorter for privileged contexts.

Use them together: a short idle timeout catches abandonment, a longer absolute cap forces a fresh proof of identity on a schedule. And beyond timeouts, deliberately force re-authentication for high-risk actions — this is step-up from the factors article, applied to the session: viewing data may ride the existing session, but changing the password, adding a payee, or exporting records should demand a fresh factor even if the session is still valid.


Choosing the numbers

There is no universal value, but the risk of the app anchors the range. As a starting point: high-sensitivity apps (banking, admin consoles) lean toward idle timeouts in the low tens of minutes and absolute caps of a few hours; ordinary productivity apps tolerate longer on both; and the more sensitive the action, the more you lean on step-up rather than a permissive global timeout. The mistake to avoid is a single generous timeout for everything — it optimizes for the convenience of the lowest-risk flow while leaving the highest-risk one exposed for just as long. Whatever numbers you pick, enforce them server-side: a client-side timer that merely hides the UI leaves the underlying session alive and replayable, which is no timeout at all. And document the rationale, because “why is this 30 minutes?” is a question auditors reliably ask.


Revocation: ending a session on demand

Timeouts end sessions eventually. Revocation ends a specific session now — on logout, on a detected compromise, when an admin disables an account, when a leaver is offboarded. How hard this is depends entirely on the stateful/stateless choice above.


  • Server-side sessions revoke cleanly: delete or flag the record, and the next request fails validation. This is the decisive operational advantage of opaque sessions, and why offboarding and incident response prefer them.
  • Stateless JWTs have no record to delete, so you need one of:
    • Short lifetimes + revocable refresh tokens — the access token expires in minutes; revoking the refresh token stops renewal. The exposure window is bounded by the access-token lifetime rather than zero.
    • A denylist of revoked token IDs (jti) checked on each request — effective, but it reintroduces the very server lookup JWTs were meant to avoid.
    • A token-version / “not-before” check per user — bump a counter to invalidate all of a user’s existing tokens at once (useful for “log out everywhere” and post-password-change).

The honest summary: instant, reliable revocation always requires some server-side state. Fully stateless sessions trade revocability for scale — a trade that is fine for low-risk APIs and dangerous for high-value accounts. Also rotate the session identifier on every privilege change — especially right after login — so a session ID captured before authentication cannot be reused after it (this defeats session fixation).


Session fixation: why you rotate the ID at login

One attack deserves its own treatment because the defense is a habit, not a flag. In session fixation, the attacker first obtains a valid session ID (many apps hand one out to anonymous visitors), then tricks the victim into authenticating under that same ID — via a planted link or a set cookie. Because the app kept the ID stable across the login, the attacker, who already knows it, now shares the victim’s authenticated session.


The fix is simple and non-negotiable: rotate the session identifier on every privilege change, above all immediately after successful authentication. Issue a brand-new session ID at login and invalidate the pre-login one. An attacker who fixed the old ID is left holding a dead value. The same rotation applies at any step-up or role elevation — whenever the trust level of the session rises, the identifier should change so anything captured at a lower level cannot ride the upgrade.


This is why the revocation section insisted on ID rotation alongside timeouts and denylists: it closes an attack that secure-cookie flags alone do not.


Persistent sessions: “remember me” done safely

A plain session cookie dies with the browser. “Remember me” deliberately extends that — a persistent cookie so the user isn’t forced to re-authenticate every day. Convenient, and a real risk if done naively, because you are now storing a long-lived credential on a device you don’t control.


Good practice for persistent login:


  • Use a separate, long-lived remember-me token, distinct from the short session token — not simply a session cookie with a huge Max-Age.
  • Make it a high-entropy random value stored server-side (hashed), so it can be revoked, and so a database leak doesn’t expose usable tokens.
  • Rotate it on each use and detect reuse of an old token as a theft signal (someone replayed a token you already rotated away) — then invalidate the whole chain.
  • Never grant a remembered session the same power as a fresh one. A remembered user can browse; changing the password, payment details, or security settings must still trigger step-up re-authentication. “Remember me” restores convenience, not full trust.

Beyond the browser: mobile apps and API sessions

Not every session is a cookie. Mobile apps and API clients usually carry a bearer token (often an OAuth access token, from the protocols module) in an Authorization: Bearer header instead of a cookie. The concept is the same — a credential that proves an authenticated session — but the defenses shift:


  • SameSite and CSRF largely fall away for pure bearer-token APIs, because the browser isn’t auto-attaching a cookie; the client explicitly sends the token. (Mixed cookie-and-token designs still need CSRF care.)
  • Storage becomes the risk. A token in browser localStorage is readable by any XSS — which is why sensitive web apps often keep the session in an HttpOnly cookie rather than JS-accessible storage. On mobile, tokens belong in the platform keystore/keychain, not plain files.
  • Short-lived access tokens plus refresh tokens are the norm (echoing the JWT discussion): the access token expires in minutes; the refresh token, held securely, obtains a new one and is itself revocable.
  • Token binding / DPoP (RFC 9449) hardens bearer tokens by cryptographically tying a token to the client’s key, so a stolen token can’t be replayed from another device — directly attacking the “bearer = whoever holds it” weakness.

The through-line: whether the session lives in a cookie or a header, it is a bearer of authenticated identity, and the job is the same — keep it from being read, replayed, or outliving its welcome.


The attacks: CSRF, XSS, and session hijacking

The session layer has three signature attacks. Knowing the control for each is core practitioner knowledge.


Session hijacking

Stealing a valid session ID and replaying it to impersonate the user. Theft routes: interception over cleartext, a cookie readable by JavaScript, leakage in URLs or logs, or lifting it from a shared machine.


Controls: Secure (no cleartext) + HttpOnly (no script access) cookies; TLS everywhere; never put session IDs in URLs; rotate the ID after login; bind sessions to context (device, coarse network) and re-verify on anomalies; short timeouts to shrink the replay window.


Cross-Site Scripting (XSS)

Injecting attacker script into a page so it runs in the victim’s browser with the site’s privileges. If a session cookie is not HttpOnly, that script can read and exfiltrate it outright; even with HttpOnly, script can ride the session to make authenticated requests.


Controls: contextual output encoding and input validation to stop injection at the source; a strong Content-Security-Policy (CSP) to constrain what scripts can run; HttpOnly so the cookie itself stays unreadable. XSS is the reason HttpOnly is non-negotiable — but note it only hides the cookie, it does not fix the underlying injection, which you must also close.


Cross-Site Request Forgery (CSRF)

Tricking a logged-in user’s browser into sending an unwanted authenticated request to your site (a hidden form or image tag firing a state-changing POST). The browser attaches the session cookie automatically, so the request looks legitimate.


Controls: SameSite=Lax/Strict cookies (blocks the classic cross-site send); anti-CSRF tokens — an unpredictable per-session/per-request value the server checks on state-changing requests; and checking Origin/Referer on sensitive endpoints. Because SameSite alone can have edge cases and older-browser gaps, layer it with anti-CSRF tokens for anything that changes state.



Where the session store lives

Choosing server-side sessions raises a practical question the JWT crowd likes to poke at: where does the state live, and does it scale? In a single-server app, in-memory sessions are trivial. The moment you run multiple instances behind a load balancer, an in-memory session on one server is invisible to the others — so a request routed elsewhere looks logged-out.


Three common answers:


  • Sticky sessions pin a user to one server. Simple, but fragile — that server’s failure logs everyone on it out, and it complicates scaling.
  • A shared session store (Redis, Memcached, a database) holds sessions centrally so any instance can read them. This is the standard modern approach: it keeps instant revocation, survives instance restarts, and scales horizontally. The cost is a fast network lookup per request and a store you must keep available.
  • Signed client-side state (the JWT end of the spectrum) removes the store entirely at the price of revocability, as covered earlier.

The point is that “server-side sessions don’t scale” is outdated folklore — a shared store like Redis handles enormous volumes while preserving the control that makes opaque sessions attractive. The real trade is operational (run a store) versus security (keep revocability), and for most high-value apps that trade favors the store.


Logout: what it should actually do

The SSO article showed how hard logout is across apps; within a single app, logout still has to be done right or it’s theater. A correct logout invalidates the session server-side — deletes or flags the record — so the credential is dead even if a copy of the cookie survives. Merely clearing the cookie in the browser is not enough: if the underlying session is still valid, a captured cookie replays straight back in.


Good logout also clears the cookie on the client (so the next visit starts clean), and ideally offers “log out everywhere” — invalidating all of a user’s sessions across devices, which the token-version technique makes cheap. And remember the boundary from SSO: logging out of the app is not the same as logging out of the IdP; the UI should be honest about which one just happened. Logout is the user’s one direct control over their own session — it must genuinely end it.


A session’s life: a worked example

Tie the pieces together with one flow. Sara logs into an online-banking web app.


  1. Login. She authenticates with a passkey. On success the server issues a fresh session ID (rotating away the anonymous one — fixation defense) and sets it as an HttpOnly; Secure; SameSite=Lax cookie. It also records a server-side session with her identity, roles, and timestamps.
  2. Browsing. Each request returns the cookie; the server looks it up and resets the idle clock. Her 15-minute idle timeout keeps sliding as long as she’s active.
  3. A sensitive action. She initiates a wire transfer. The app doesn’t trust the standing session for this — it forces step-up, re-verifying her passkey right now, and could rotate the session ID again on this elevation.
  4. She walks away. After 15 minutes of inactivity the idle timeout ends the session server-side; her next click lands on a login page. Even if she’d stayed active, an 8-hour absolute timeout would eventually force a fresh login.
  5. Reported theft. That evening she reports a lost laptop. Support revokes her sessions server-side (delete the records / bump her token version); any cookie still on the laptop now fails validation on the next request. Because the bank chose server-side sessions, this is instant — a stateless-JWT design would have to wait out the token’s lifetime or hit a denylist.

Every mechanism in this article — cookie flags, ID rotation, idle vs absolute timeouts, step-up, server-side revocation — shows up in one ordinary, defensible session.


A session-hardening checklist

A compact review list you can run against any application:


  • Cookie flags: HttpOnly, Secure, SameSite=Lax/Strict, narrow Domain/Path, no needless persistence.
  • Session ID: high-entropy, opaque, rotated at login and every privilege change, never placed in a URL.
  • Timeouts: a short idle timeout and a longer absolute cap, both enforced server-side.
  • Step-up: sensitive actions re-authenticate even within a valid session.
  • Revocation: a reliable server-side path to end a session now (logout, incident, offboarding); for JWTs, short lifetimes + revocable refresh tokens.
  • XSS defenses: output encoding, input validation, and a strong CSP — because HttpOnly limits but does not remove XSS.
  • CSRF defenses: SameSite plus anti-CSRF tokens on state-changing requests.
  • Transport: TLS everywhere; never transmit or log a session ID in cleartext.
  • Persistent login: separate, rotating, revocable remember-me tokens with reduced privilege.
  • Non-browser clients: tokens in secure storage, short-lived access + refresh, consider DPoP/token binding.

Recap

The session is authentication’s afterlife — and a prime target — so it deserves deliberate engineering:


  1. Cookies carry the session; the security is in the attributes — HttpOnly, Secure, SameSite, plus tight Domain/Path.
  2. Server-side tokens vs JWTs is a trade of revocability and control against statelessness and scale; a long-lived JWT as the whole session is an anti-pattern.
  3. Idle and absolute timeouts run together — idle catches abandonment, absolute caps lifetime and forces periodic re-authentication.
  4. Revocation is instant with server-side state and genuinely hard for stateless JWTs; true “log out now” always needs some server state, plus session-ID rotation to defeat fixation.
  5. CSRF, XSS, and session hijacking each have specific, interlocking controls — no single flag is sufficient.


Three questions to test yourself

  1. A colleague proposes storing the whole user profile and roles in a JWT valid for 24 hours, “to avoid a session database.” Give two concrete scenarios where this bites you, and how you’d redesign it while keeping most of the scalability.
  2. Design the timeout policy for an online-banking app: pick an idle timeout, an absolute timeout, and name two actions that should force step-up re-authentication even mid-session. Justify each number.
  3. An app sets its session cookie without HttpOnly and without SameSite. Name the specific attack each missing attribute exposes, and the additional control you’d add beyond just fixing the flags.

Hands-on exercises

  1. Inspect a real session cookie. On a site you use, open dev-tools and read the session cookie’s attributes. Note which of HttpOnly, Secure, and SameSite are set. Write one sentence on the risk of any that are missing.
  2. Decode vs look up. Find a JWT (many apps expose one in local storage or an Authorization header) and decode its payload at a JWT debugger. Identify its exp. Ask yourself: if this token were stolen right now, how long would it stay valid, and could the server stop it sooner?
  3. Trace a logout. Log out of a web app with the network tab open. Determine whether the session ended server-side (the old cookie now fails) or merely got cleared client-side (the value would still validate if replayed). Which did the app do?

Frequently asked questions

What do the HttpOnly, Secure, and SameSite cookie attributes do?

HttpOnly hides the cookie from JavaScript so an XSS payload cannot read the session ID. Secure sends the cookie only over HTTPS, preventing interception in cleartext. SameSite (Strict, Lax, or None) controls whether the cookie is attached to cross-site requests, which is the main browser-level defense against CSRF. A session cookie should be HttpOnly, Secure, and SameSite=Lax or Strict.

What is the difference between a server-side session token and a JWT for sessions?

A server-side session token is an opaque random ID that points to session state stored on the server, so the server is the source of truth and can revoke instantly by deleting the record. A JWT is a self-contained, signed token the server validates without a lookup, which scales well but is hard to revoke before it expires because nothing is stored to delete. The trade-off is revocability and control versus statelessness and scale.

What is the difference between an idle timeout and an absolute timeout?

An idle (sliding) timeout ends the session after a period of inactivity and resets on each request, so an active user stays logged in. An absolute timeout ends the session a fixed time after login regardless of activity, capping the total lifetime. Robust systems use both: a short idle timeout to catch abandoned sessions and a longer absolute cap to force periodic re-authentication.

How do you revoke a session before it expires?

With server-side sessions you delete or flag the session record and the next request fails validation immediately. With stateless JWTs you cannot delete anything, so you either keep short token lifetimes with revocable refresh tokens, or maintain a denylist / token-version check that reintroduces a server lookup. True instant revocation always requires some server-side state.

What are the main attacks against sessions?

Session hijacking (stealing a valid session ID via interception, XSS, or an insecure cookie), cross-site scripting (XSS, injecting script that reads or abuses the session), and cross-site request forgery (CSRF, tricking the browser into sending an authenticated request). The core defenses are HttpOnly/Secure/SameSite cookies, output encoding and CSP against XSS, anti-CSRF tokens, and rotating the session ID on privilege changes.