Table of Contents

    JWT and sessions

    SYSTEM DESIGN • CHAPTER 13.3

    JWT and Sessions

    Understand how JSON Web Tokens and server-side sessions each maintain authenticated state, where their guarantees differ, and how to choose between them without creating revocation or storage problems.

    Learning objective: By the end of this article, you will understand JWT structure and validation, server-side session mechanics, the revocation trade-off between them, secure cookie configuration, and the hybrid pattern most production systems actually use.

    Prerequisites

    Recommended Knowledge

    • HTTP request and response headers
    • Cookies and browser storage mechanisms
    • TLS and transport security
    • Authentication versus authorization
    • OAuth 2.0 and OpenID Connect token types
    • Hashing, signatures, and public-key cryptography
    • Base64 URL encoding and JSON structure
    • Caching and distributed data stores
    • Load balancing and stateless service design

    The Underlying Problem

    HTTP is stateless. Each request arrives independently, carrying no inherent memory of previous requests. Once a user authenticates, the system needs some mechanism to recognize them on every subsequent request without asking for credentials again.

    Two dominant approaches exist. The server can remember the session and give the client a reference to it, or the server can hand the client a signed statement that it verifies on each request.

    TWO STRATEGIES
    Server Remembers Session ID

    Client Carries Proof Signed Token

    Simple Analogy

    A session is a coat-check ticket. The number means nothing by itself, but the attendant's register maps it to your coat, and they can cancel it instantly. A JWT is a signed pass printed with your details, readable anywhere, but difficult to recall once handed out.

    Server-Side Sessions

    In the session model, the server creates a record after successful authentication and returns only an unguessable identifier to the client, usually in a cookie. The authoritative state stays on the server.

    SESSION LIFECYCLE
    Login Create Record Return Cookie Look Up per Request
    CREATE TABLE user_sessions (
        session_id       VARCHAR(128) PRIMARY KEY,
        user_id          BIGINT       NOT NULL,
        created_at       TIMESTAMP    NOT NULL,
        last_seen_at     TIMESTAMP    NOT NULL,
        absolute_expiry  TIMESTAMP    NOT NULL,
        idle_expiry      TIMESTAMP    NOT NULL,
        ip_address       VARCHAR(45)  NULL,
        user_agent_hash  VARCHAR(64)  NULL,
        auth_level       VARCHAR(20)  NOT NULL,
        revoked_at       TIMESTAMP    NULL
    );
    
    CREATE INDEX idx_sessions_user
        ON user_sessions (user_id, revoked_at);

    Storing the user identifier alongside the session allows a critical operation: terminating every active session for one user in a single statement.

    UPDATE user_sessions
    SET revoked_at = CURRENT_TIMESTAMP
    WHERE user_id = :user_id
      AND revoked_at IS NULL;

    Session Identifier Requirements

    A Safe Session ID

    • Generated from a cryptographically secure random source
    • Long enough to make guessing infeasible
    • Carries no encoded user information
    • Regenerated immediately after login
    • Regenerated after any privilege change
    • Stored hashed if the datastore is a lower trust tier
    Session Fixation Risk Reusing the pre-login session identifier after authentication lets an attacker who planted that identifier inherit the authenticated session.
    Correct Behavior Issue a brand-new identifier on successful login, invalidate the previous one, and repeat this whenever the authentication level changes.

    JSON Web Tokens

    A JWT is a self-contained, signed statement. It carries its claims in the token itself, so a recipient holding the correct key can verify it without consulting a central store.

    JWT STRUCTURE
    Header . Payload . Signature
    {
        "alg": "RS256",
        "typ": "JWT",
        "kid": "signing-key-2026-09"
    }
    {
        "iss": "https://identity.example.com",
        "sub": "user-4821",
        "aud": "orders-api",
        "exp": 1790000900,
        "iat": 1790000000,
        "jti": "tok-9c41e7f2",
        "scope": "orders.read orders.write",
        "auth_level": "mfa"
    }
    Part Contents Protection Provided
    Header Algorithm, type, and key identifier Tells the verifier which key and algorithm apply
    Payload Claims about subject, audience, and validity None by itself, the content is merely encoded
    Signature Cryptographic signature over header and payload Detects any modification of the preceding parts
    Critical misconception: A signed JWT is not encrypted. Anyone holding the token can decode and read every claim. Signing provides integrity, never confidentiality.
    Never Place in a JWT Passwords, full payment details, government identifiers, internal database keys, security answers, or any information that would cause harm if read by whoever holds the token.

    Direct Comparison

    Dimension Server-Side Session JSON Web Token
    State location Server datastore Inside the token itself
    Validation Lookup in the session store Local signature verification
    Revocation Immediate Effective only at expiry unless supplemented
    Per-request cost One datastore read One signature verification
    Horizontal scaling Requires a shared session store No shared state needed for verification
    Payload visibility Nothing exposed to the client All claims readable by the holder
    Size on the wire Small identifier Grows with the number of claims
    Updating permissions Takes effect on the next request Takes effect after the token refreshes
    Cross-service use Needs shared access to the store Any service with the public key can verify
    Failure coupling Session store is a dependency Independent of the issuer at request time
    The central trade-off is not performance. It is that a session can be cancelled the instant you need it cancelled, while a JWT generally cannot.

    The Revocation Problem

    Because a JWT is verified locally, the verifier has no natural way to learn that the token should no longer be honoured. Until the expiry claim passes, it remains cryptographically valid.

    EXPOSURE WINDOW
    \[ W_{\text{exposure}} = T_{\text{exp}} - T_{\text{revocation decision}} \]

    This window matters in concrete situations: a user logs out, an account is suspended, a device is reported stolen, a role is downgraded, or an administrator detects a compromise.

    Mitigation Mechanism Cost
    Short lifetimes Bound the exposure window by expiry More frequent refresh traffic
    Denylist by token ID Check the identifier against revoked entries Reintroduces a lookup on each request
    User-level version claim Compare a token version against the current one Requires a cached version lookup
    Introspection Ask the issuer whether the token is active Network dependency per request
    Refresh token rotation Revoke renewal rather than the access token Existing access token still valid briefly

    Version-Claim Revocation

    async function isTokenStillValid(claims) {
        const cacheKey = `session-version:${claims.sub}`;
    
        let currentVersion = await cache.get(cacheKey);
    
        if (currentVersion === null) {
            currentVersion = await userRepository
                .getSessionVersion(claims.sub);
    
            await cache.set(cacheKey, currentVersion, {
                ttlSeconds: 60
            });
        }
    
        return Number(claims.session_version) === Number(currentVersion);
    }

    Incrementing the stored version invalidates every outstanding token for that user. The cache TTL determines how quickly the change propagates, which is a deliberate freshness decision.

    HONEST OBSERVATION
    Every JWT revocation mechanism reintroduces some form of lookup. If instant revocation is mandatory, a session model may be the simpler and more honest design.

    Validating a JWT Properly

    async function verifyAccessToken(token, config) {
        const segments = token.split(".");
    
        if (segments.length !== 3) {
            throw new Error("Malformed token");
        }
    
        const header = decodeSegment(segments[0]);
    
        if (!config.allowedAlgorithms.includes(header.alg)) {
            throw new Error("Algorithm not permitted");
        }
    
        const key = await keyStore.getKey(header.kid);
    
        if (!key) {
            throw new Error("Unknown signing key");
        }
    
        const signatureValid = await verifySignature(
            `${segments[0]}.${segments[1]}`,
            segments[2],
            key,
            header.alg
        );
    
        if (!signatureValid) {
            throw new Error("Signature verification failed");
        }
    
        const claims = decodeSegment(segments[1]);
        const nowSeconds = Math.floor(Date.now() / 1000);
        const skew = config.clockSkewSeconds;
    
        if (claims.iss !== config.expectedIssuer) {
            throw new Error("Issuer mismatch");
        }
    
        const audiences = Array.isArray(claims.aud)
            ? claims.aud
            : [claims.aud];
    
        if (!audiences.includes(config.expectedAudience)) {
            throw new Error("Audience mismatch");
        }
    
        if (claims.exp + skew < nowSeconds) {
            throw new Error("Token expired");
        }
    
        if (claims.nbf && claims.nbf - skew > nowSeconds) {
            throw new Error("Token not yet valid");
        }
    
        if (await denyList.contains(claims.jti)) {
            throw new Error("Token revoked");
        }
    
        return claims;
    }
    Algorithm Confusion Attack Letting the token's own header choose the verification method allows an attacker to switch a public-key algorithm to a symmetric one and sign tokens using the public key as the secret.
    Correct Defense Maintain a server-side allow-list of acceptable algorithms, reject the none algorithm unconditionally, and never derive the verification strategy from untrusted input.

    Cookie Configuration

    Whether you carry a session identifier or a token, cookie attributes determine much of the practical security posture.

    Set-Cookie: sid=9f2b71c4a83e5d06;
        HttpOnly;
        Secure;
        SameSite=Lax;
        Path=/;
        Max-Age=1800
    Attribute Effect Threat Addressed
    HttpOnly Blocks script access to the cookie Token theft via cross-site scripting
    Secure Sends only over encrypted connections Interception over plaintext transport
    SameSite Limits sending on cross-site requests Cross-site request forgery
    Path Restricts the cookie to a path prefix Unnecessary exposure across the site
    Domain Controls subdomain sharing Leakage to less trusted subdomains
    Max-Age Bounds the cookie lifetime Indefinite reuse of stale credentials

    Where Browser Clients Should Store Credentials

    Location Script Accessible Assessment
    Local storage Yes Any script compromise exposes the credential
    Session storage Yes Same exposure, merely shorter lived
    Regular cookie Yes Readable by scripts, offers little advantage
    HttpOnly cookie No Preferred for browser-facing credentials
    In-memory variable Yes Lost on reload, still script reachable

    Common Justification

    • Local storage avoids CSRF entirely
    • It is convenient for attaching headers
    • Many tutorials demonstrate this approach

    Why It Is Weak

    • CSRF has well-established defenses
    • Script-based theft has no equivalent recovery
    • One compromised dependency exposes every token
    • HttpOnly cookies remove the theft vector entirely

    Cross-Site Request Forgery

    Cookies are attached automatically by the browser, which is convenient and also the root of CSRF. A malicious page can trigger a request that carries the victim's cookie.

    Layered CSRF Defenses

    • Set SameSite on session cookies
    • Use anti-forgery tokens on state-changing requests
    • Adopt the double-submit cookie pattern where appropriate
    • Verify Origin and Referer headers on sensitive endpoints
    • Require safe HTTP methods to remain side-effect free
    • Require re-authentication for high-risk operations

    Expiry Policies

    A robust design applies more than one expiry rule, because idle abandonment and long-running compromise are different risks.

    1

    Idle Timeout

    Ends the session after a period without activity, limiting exposure when a device is left unattended.

    2

    Absolute Timeout

    Ends the session a fixed duration after creation regardless of activity, bounding the value of a stolen credential.

    3

    Renewal Window

    Allows extension while activity continues, without permitting indefinite life beyond the absolute limit.

    4

    Step-Up Requirement

    Demands fresh or stronger authentication before privileged operations, even within a valid session.

    EFFECTIVE SESSION END
    \[ T_{\text{end}} = \min(T_{\text{idle}}, T_{\text{absolute}}) \]

    The Hybrid Pattern

    Most mature systems do not choose one model exclusively. They combine a server-managed session at the edge with short-lived tokens for internal calls.

    HYBRID ARCHITECTURE
    Browser Session Cookie Edge Service Short-Lived JWT Internal APIs
    1. The user authenticates and receives an HttpOnly session cookie.
    2. The edge service holds the authoritative session record.
    3. Each request resolves the session and confirms it is active.
    4. The edge mints a short-lived token scoped to the downstream call.
    5. Internal services verify that token locally without a lookup.
    6. Logout revokes the session, ending new token issuance at once.

    What This Gains

    • Immediate revocation at the boundary
    • No tokens exposed to browser scripts
    • Stateless verification inside the service mesh
    • Narrow, per-call token scoping
    • Central place for session monitoring

    What It Costs

    • An additional component to operate
    • Session store becomes a critical dependency
    • Token minting adds edge latency
    • Key distribution must be managed

    Choosing Between Them

    Prefer Sessions When Immediate revocation is required, the client is a browser, permissions change frequently, sensitive attributes must stay server-side, or you need visibility into every active login.
    Prefer JWTs When Many independent services must verify identity, the caller is a machine workload, cross-domain or mobile clients are involved, or a shared session store would become a bottleneck.
    Prefer Hybrid When You need both browser-facing revocation control and stateless verification across a distributed backend, which describes most large systems.

    Signing Key Management

    Aspect Symmetric Signing Asymmetric Signing
    Key material One shared secret Private signing and public verification keys
    Verifier needs The same secret used to sign Only the public key
    Forgery risk Any verifier can also mint tokens Verifiers cannot mint tokens
    Best fit A single trusted service Multiple independent services

    Key Rotation Practices

    • Include a key identifier in every token header
    • Publish the current public key set for verifiers
    • Accept the previous key during an overlap period
    • Cache keys and refresh on unknown identifiers
    • Rotate on a schedule and after any suspected exposure
    • Store private keys in a managed secret or key service
    • Alert on verification failures caused by missing keys

    Threats and Mitigations

    Threat Description Mitigation
    Session fixation Attacker plants an identifier before login Regenerate the identifier after authentication
    Session hijacking Credential stolen and replayed TLS, HttpOnly cookies, anomaly detection
    Algorithm confusion Header manipulated to weaken verification Server-side algorithm allow-list
    Unsigned token acceptance The none algorithm is honoured Reject unsigned tokens unconditionally
    Claim tampering Payload edited to escalate privilege Verify the signature before reading claims
    Token replay Captured token reused elsewhere Short expiry, audience binding, unique identifiers
    Stale authorization Revoked permissions remain in an active token Short lifetimes or a version claim check
    Sensitive claim exposure Private data readable in the payload Keep sensitive attributes server-side
    Cross-site scripting Script reads a browser-stored credential HttpOnly cookies and content security policy
    Session store outage Authentication fails system-wide Replication, caching, and degradation planning

    Monitoring and Detection

    Signals Worth Tracking

    • Active session count per user
    • Signature verification failures by cause
    • Expired token presentation rate
    • Audience and issuer mismatch attempts
    • Sessions used from unexpected locations
    • Concurrent use of one session from distant addresses
    • Session store latency and error rate
    • Revocation events and their propagation delay
    • Denylist size and lookup performance
    • Logout completion rate

    Common Design Mistakes

    Weak Design

    • Choosing JWTs purely to avoid a session store
    • Issuing tokens valid for days or weeks
    • Placing sensitive attributes in the payload
    • Storing tokens in local storage
    • Trusting claims before signature verification
    • Reusing the identifier across login
    • Treating logout as a client-side action only
    • Applying a single expiry rule
    • Ignoring key rotation until an incident

    Strong Design

    • Chooses based on revocation requirements
    • Keeps access credentials short-lived
    • Keeps sensitive attributes server-side
    • Uses HttpOnly, Secure, SameSite cookies
    • Verifies signature, issuer, audience, and expiry
    • Regenerates identifiers on privilege change
    • Revokes server-side on logout
    • Applies idle and absolute timeouts together
    • Rotates keys with an overlap window

    System Design Interview Discussion

    Question What Your Answer Should Cover
    Sessions or JWTs, and why? Revocation needs, client type, and service topology
    How does logout work? Server-side revocation and the residual token window
    How do you revoke a JWT? Short expiry, version claim, denylist, or introspection
    Where is the credential stored? HttpOnly cookie reasoning versus browser storage
    How does this scale? Shared store versus local verification trade-offs
    What if the session store fails? Replication, caching, and degradation behavior
    How are keys managed? Rotation, key identifiers, and overlap periods
    How is compromise detected? Anomaly signals and validation failure monitoring

    Implementation Checklist

    Production Checklist

    • Generate identifiers from a secure random source
    • Regenerate the identifier after every login
    • Set HttpOnly, Secure, and SameSite on cookies
    • Apply both idle and absolute expiry
    • Verify signature before reading any claim
    • Allow-list signing algorithms server-side
    • Validate issuer, audience, and time claims
    • Include a key identifier and support rotation
    • Keep access token lifetimes short
    • Provide a genuine server-side logout
    • Support revoking all sessions for one user
    • Exclude sensitive data from token payloads
    • Enforce TLS on every authenticated endpoint
    • Require step-up authentication for privileged actions
    • Monitor validation failures and unusual session use
    • Test expired, tampered, and wrong-audience credentials

    Knowledge Check

    1

    Why is a JWT harder to revoke than a session?

    It is verified locally using a signature, so the verifier never consults a central record that could mark it invalid before expiry.

    2

    Does signing keep a JWT payload private?

    No. Signing provides integrity only. Anyone holding the token can decode and read every claim inside it.

    3

    Why regenerate the session identifier at login?

    To prevent session fixation, where an attacker supplies an identifier beforehand and inherits the authenticated session.

    4

    Why prefer an HttpOnly cookie over local storage?

    Local storage is readable by any script on the page, so one scripting vulnerability or compromised dependency exposes the credential.

    5

    What does the hybrid pattern achieve?

    Immediate revocation at the browser boundary combined with stateless, lookup-free verification across internal services.

    Summary

    Sessions and JWTs both maintain authenticated state across stateless HTTP requests, but they place that state in different locations. A session keeps authority on the server and hands out a meaningless reference. A JWT hands the client a signed statement that any holder of the verification key can validate.

    The decisive difference is revocation. Sessions can be terminated instantly, while JWTs remain valid until they expire unless a lookup mechanism is reintroduced, which partially negates their stateless advantage.

    Correct JWT handling requires verifying the signature before trusting any claim, enforcing a server-side algorithm allow-list, and validating issuer, audience, and time claims. Correct session handling requires unguessable identifiers, regeneration at login, layered expiry, and genuine server-side logout.

    In browsers, credentials belong in HttpOnly, Secure, SameSite cookies rather than local storage. Most large systems adopt a hybrid: a revocable session at the edge, and short-lived tokens verified locally by internal services.

    Key Takeaway

    Choose based on revocation, not convenience. Sessions give you immediate control at the cost of a shared store. JWTs give you stateless verification at the cost of a delay between deciding to revoke and that decision taking effect. Keep lifetimes short, keep sensitive data out of payloads, and keep credentials out of reach of browser scripts.