Skip to content

Lesson 3 of 12 · 7 min read

Sessions, Cookies and Tokens

Free lessonReviewed By the AISecurityInterviews editorial team
Reading position
In this lesson

TL;DR: A server-side session uses an opaque identifier to retrieve current state. A bearer credential may be opaque or structured; possession lets a caller attempt the operations that its scope and the service policy permit. Protect browser cookies, enforce expiry on the server, and test revocation at every accepting service. Signed tokens permit local validation, but local validation alone does not establish current authorization.

Before you read

Pick an answer before you read further. The lesson below explains it, and guessing first, right or wrong, helps the explanation stay with you.

  1. 1An application stores its JWT access token in localStorage. A cross-site scripting bug is found on one page. What can the attacker do that they could not with an HttpOnly cookie?

The problem after login

HTTP is stateless. Once a user has authenticated, every later request needs to carry proof of who they are. There are two broad designs, and most systems mix them.

Server-side sessions

The server creates a session record (user ID, creation time, expiry, maybe roles) and stores it in memory, a database or a cache. The client gets a long random identifier, usually in a cookie. On each request the server looks the identifier up.

  • Theft: whoever steals the identifier can use the session until it ends.
  • Revocation: invalidate the authoritative record, then account for replicas, cached positive decisions and in-flight requests. Revocation latency depends on those paths.
  • Cost: a lookup per request, and shared session storage for multiple servers.

Two classic bugs: session fixation, where the server keeps the same identifier from before login so an attacker who planted it gains access; fix it by issuing a new identifier at login and privilege changes. And predictable identifiers, fixed by using a cryptographic random generator with at least 128 bits.

Entropy and lifetime are separate requirements

Use a cryptographically secure random generator for opaque session identifiers. Predictable timestamps or counters do not contribute secret entropy merely because they make an identifier long. For this site's examples, choose at least 128 random bits; follow the chosen framework's documented session mechanism rather than designing a new token format.

Design propertyEvidence to request
Unpredictable identifierApproved cryptographic generator and sufficient random bits
Rotation at authenticationThe pre-login identifier cannot access the authenticated session
Idle timeoutThe server rejects an otherwise valid session after inactivity
Absolute timeoutContinued activity cannot extend the session indefinitely
WithdrawalThe authoritative session and dependent acceptance paths stop permitting access

The OWASP session management guidance distinguishes these controls. Select timeouts for the application's risk and workflow, enforce them server-side, and document the residual window introduced by caches. Browser cookie expiry is not a substitute for server expiry.

Bearer tokens

Bearer describes how a credential is used, not its serialization. An opaque access token may require an authorization-server lookup. A signed JWT exposes its claims to its holder and can be checked locally; an encrypted JWT has different confidentiality properties. OAuth does not require JWT access tokens.

A stolen bearer credential may be usable until expiration or effective withdrawal, subject to audience, scope and other enforced policy. Local signature verification cannot detect a role change or logout by itself. Introspection, current authorization checks, revocation state or short lifetimes can bound that gap, with different availability and consistency costs.

Refresh-token revocation prevents future issuance through that grant once enforced; it does not automatically recall access tokens already issued. Refresh rotation also needs reuse handling and a deliberate policy for concurrent legitimate refreshes. Keep token values out of logs.

Cookies: the attributes that matter

Cookies are sent automatically by the browser, which is both convenient and the source of their risks.

AttributeWhat it doesStops
HttpOnlyJavaScript cannot read the cookieToken theft via XSS (not XSS itself)
SecureSent only over HTTPSTheft on plain-HTTP connections
SameSite=Lax or StrictNot sent on most cross-site requestsMost cross-site request forgery
__Host- name prefixRequires Secure, path /, no Domain attributeSubdomains overwriting the cookie
Short Max-Age with idle timeoutLimits how long a stolen session is usefulLong-lived stolen sessions

For browser applications, an HttpOnly, Secure, SameSite session cookie is usually safer than a token in JavaScript-accessible storage.

Set attributes explicitly. Prefix enforcement and partitioned-cookie support depend on the browser, so verify the supported client set rather than assuming a name alone establishes the policy. The browser cookie reference documents the attributes and their conditions. A __Host- cookie constrains domain and path settings in supporting browsers; it does not stop script already executing in the application from invoking authenticated actions.

Keep the server's session lifetime authoritative. A browser may retain, restore or discard cookies independently of that policy. Logout must invalidate server authority as well as clearing the browser cookie.

Where tokens live in a browser app

For single-page applications, the IETF's best current practice for browser-based OAuth clients, RFC 10017 (August 2026), recommends a backend-for-frontend (BFF): a small server-side component that is the confidential OAuth client, keeps access and refresh tokens on the server, and gives the browser only an HttpOnly, __Host- session cookie. The browser calls the BFF, which calls the APIs. OAuth tokens stay out of page JavaScript in this design. Active malicious script can still invoke allowed BFF operations through the browser session, so the BFF needs CSRF protection, constrained proxy destinations and resource authorization. Where a BFF is not possible, a token-mediating backend that hands short-lived access tokens to the page is the fallback, ideally bound to a non-extractable key with DPoP; storing long-lived tokens in localStorage is the pattern to avoid.

LESSON · SESSIONS, COOKIES AND TOKENS
An XSS bug lands in a single-page app. What can it take?

Tokens in localStorage

Injected script runs

Reads access and refresh tokens

readable by any script

Copied tokens may outlive the page

Outcome, unsafe: Account taken over off-site

Residual access depends on expiry, binding and withdrawal.

Backend-for-frontend

Injected script runs

Only an HttpOnly session cookie

tokens stay on the server

Acts in-page while open

Outcome, safe: OAuth tokens stay server-side

In-page actions can still disclose data or make lasting changes.

1/2

Tokens in the page

Any script that runs can read and export long-lived tokens.

Constrain use of a copied token

Bearer tokens have a structural weakness: possession is enough. Two standards can require possession of an additional key. A copied token alone then fails when the resource server correctly validates the binding and proof:

  • DPoP (RFC 9449): the client proves possession of a private key with a signed header on each request.
  • Mutual TLS-bound tokens (RFC 8705): the token is bound to the client's TLS certificate.

These controls do not protect against every use of a compromised client runtime. If an unauthorized process can invoke the key operation, non-exportability alone does not prevent misuse. Never use logs as token storage.

LESSON · SESSIONS, COOKIES AND TOKENS
A token leaks into a log: bearer versus sender-constrained

Log file

Token copied here

Attacker

Has the token, not the key

API

Checks the token

Also in the picture

Legitimate client

  • Holds private key
1/2

Plain bearer token

The attacker copies the token from the log and replays it. The API sees a valid signature and unexpired claims, and accepts it.

Run a session lifecycle acceptance fixture

The following fictional policy permits fifteen minutes of inactivity and eight hours of total session age. Times are integer seconds on an exercise clock. The function consumes a fresh authoritative record; it does not parse cookies, authenticate users or demonstrate a distributed cache. A missing record fails closed for this operation.

from dataclasses import dataclass, replace

@dataclass(frozen=True)
class Session:
    created: int
    last_seen: int
    revoked: bool = False

def decision(session, now):
    if session is None or session.revoked:
        return "deny: withdrawn"
    if not session.created <= session.last_seen <= now:
        return "deny: clock or record"
    if now - session.created >= 8 * 3600:
        return "deny: absolute expiry"
    if now - session.last_seen >= 15 * 60:
        return "deny: idle expiry"
    return "allow"

active = Session(100, 1000)
print(decision(active, 1100))  # => allow
print(decision(replace(active, revoked=True), 1100))  # => deny: withdrawn
print(decision(active, 1900))  # => deny: idle expiry
print(decision(Session(100, 28899), 28900))  # => deny: absolute expiry
print(decision(None, 1100))  # => deny: withdrawn

After an allowed request, update activity according to the documented policy. Batching activity writes changes the effective idle behavior. Define atomicity for concurrent logout and refresh so a stale writer cannot recreate a withdrawn session. At a sensitive state change, decide whether permission must be checked again at commit; a request admitted before logout may otherwise finish afterward.

Now keep a copy of active in a resource cache and mark only the authoritative record revoked. The cached copy still returns allow. That is the acceptance test your distributed design must address. Choose a bounded cache lifetime, an authenticated invalidation mechanism, or an authoritative check for the sensitive operation. State how an unavailable session store affects the operation rather than silently permitting indefinitely stale decisions.

Diagram
Logout propagation has observable boundaries

Browser

Session store

Resource service

  1. Browser to Session store

    1Invalidate the session
1/3

1. Invalidate the session

Identify the session or family and record withdrawal.

Clearing the browser cookie alone does not change what a resource service accepts.

Misconceptions interviewers listen for

  • "HttpOnly stops XSS." It stops script reading the cookie; injected script can still act as the user in the page.
  • "SameSite replaces CSRF tokens." It is defence in depth: browser defaults differ, the two-minute POST exception exists, and subdomains are same-site.
  • "A JWT in localStorage is stateless and therefore safer." Any script in the page can read and export it.
  • "Logging out deletes the cookie." The server must invalidate the session, or a copied cookie keeps working.
  • "A long session ID is secure." Only random bits count; fixed or predictable parts do not.

What if · interview follow-ups

Hold your answer as the constraints change

You are reviewing session handling for a banking web app built as a single-page application on app.example.com, with marketing pages on www.example.com and a blog on a third-party platform at blog.example.com. The interviewer changes one detail at a time.

  1. Start here

    What if session IDs are 32 hex characters but the first 16 encode the server and a timestamp?

How this shows up in interviews

"Sessions or JWTs?" is a common design question, and "it depends" scores only if you name what it depends on: revocation needs, number of services verifying, browser versus server clients, and the theft model. Strong answers mention cookie attributes by name, the localStorage-plus-XSS problem, refresh token rotation, and sender-constrained tokens for high-risk clients such as agents.

Say it in 30 seconds: "A session is continuing authority. I protect its identifier, rotate it at authentication, enforce idle and absolute expiry on the server, and measure withdrawal at accepting services. JWT is a format, bearer is a usage model, and a valid signature is not evidence that permission is still current."

Check yourself

Say or write your answer first, then reveal. Explaining it from memory, in your own words, is the skill an interviewer is checking.

  1. 1Why should a server issue a new session ID when a user logs in, even if the browser already had one?

  2. 2A user is removed from a project, but their JWT access token says they are a member and is valid for another 12 hours. What would you change?

  3. 3A 16-hex-character session ID has 8 fixed characters. How many random bits remain, and why does it matter?

  4. 4What does RFC 10017 recommend for where a single-page app's tokens live?

Practise it as an interviewer asks it

Course lessons that teach this

Related lessons