In short

Issue a new random session ID of at least 128 bits after every login and reauthentication, in a __Host- cookie with Secure, HttpOnly, SameSite=Lax or Strict and Path=/. Enforce idle and absolute timeouts on the server, such as 30 minutes and 12 hours; NIST recommends at most 1 hour and 24 hours at AAL2. Require fresh authentication for sensitive changes, and revoke sessions on logout, password reset and account disable.

  • A session cookie is a bearer credential: whoever copies it is the user until it expires or is revoked.
  • Rotate the identifier on login, reauthentication and privilege change, carrying over only approved anonymous state.
  • Cookie: __Host- prefix, Secure, HttpOnly, SameSite=Lax or Strict, Path=/ and no Domain attribute.
  • Enforce timeouts on the server. NIST SP 800-63B-4 limits: 1 hour idle and 24 hours overall at AAL2 (recommended); 15 minutes idle (recommended) and 12 hours overall (required) at AAL3.
  • Revoke through a server-side session version on password reset, MFA change, account disable or role reduction.
  • Against cookie-stealing malware, keep sessions short, reauthenticate for sensitive actions and bind sessions to the device with DBSC where supported.

Verify it

  1. Log in and confirm the Set-Cookie header carries the __Host- prefix, Secure, HttpOnly, SameSite and Path=/, and a new identifier.
  2. Replay the pre-login cookie after login, and a copied cookie after logout: both must fail.
  3. Leave a session idle until the timeout and keep another active past the absolute limit: both must require login.
  4. Reset the password on one device and confirm sessions on other devices stop working on their next request.
  5. Try to change the account email or add an authenticator from an old session: the server must ask for authentication first.
Applies to

Login, session renewal, logout and revocation for browser sessions. Token lifetimes and browser cookie policy are separate controls.

Fix it in your stack

Tested fixes: the vulnerable code, the fix, a plausible fix that still fails, and the regression tests that tell them apart.

Learning goals and prerequisitesAbout 31 minutes: 9 to read and 22 for the exercise

What you will learn

  • Distinguish authentication events from reusable sessions.
  • Explain fixation and independent expiry clocks.
  • Test revocation after logout and credential changes.

Before you start

Developers who implement login and sessions, and reviewers who check cookie settings, timeouts and revocation.

  • Understand cookies, bearer credentials and server-side session state.
Go to the guided practice

A login event creates authority that survives the password check

Authentication establishes who a caller is for a particular event. A session lets later requests reuse that result. The session credential is therefore an authority-bearing secret even when it contains no password. Its storage, scope, expiry, rotation, and invalidation determine how long a successful theft or fixation remains useful.

Start with explicit states: anonymous, partially authenticated, fully authenticated, recently reauthenticated, expired, and revoked. MFA completion must be a transition enforced by the server. A client-side flag saying mfaComplete=true cannot create it. Sensitive actions can require a recent authentication event even when the ordinary session remains valid.

An opaque cookie can identify server-side session state. A self-contained token can carry claims but is harder to revoke immediately without state, short expiry, or an online check. Choose based on the application’s revocation needs and operational constraints. A signed token protects integrity; it does not prevent its bearer from replaying it.

Session cookies or tokens: which should a web app use?

For a browser-based web application, use an opaque, random session ID in a __Host- cookie marked Secure, HttpOnly and SameSite, and keep the session state on the server. Use self-contained tokens such as JWT access tokens between services and for APIs called by non-browser clients. If a single-page app calls APIs that need tokens, put a backend for frontend in front of them, so the browser still holds only a session cookie and the tokens stay on the server.

The deciding difference is revocation. Deleting a server-side session ends it on the next request; a self-contained token stays valid until it expires unless every verifier checks extra state. Storage is the second difference: an HttpOnly cookie cannot be read by script, while a token kept in localStorage can be read by any script that runs in the page. Cookies bring their own duty, cross-site request forgery, which SameSite and a CSRF defense handle; the CSRF lab at /labs/csrf/ practices one.

Session cookies or tokens: which should a web app use?
QuestionServer-side session in a cookieSelf-contained token held by the browser
End a session now (logout, password reset, account disabled)Delete or version the session; effective on the next requestWaits for expiry unless verifiers check a deny list or session version
Theft by injected script (XSS)An HttpOnly cookie cannot be read, though the script can still send requests as the userA token in localStorage or sessionStorage can be read and sent elsewhere
Cross-site request forgeryNeeds SameSite and a CSRF token or Origin checkNot sent automatically when sent in a header, so no CSRF; XSS is the risk instead
ScalingNeeds a shared session storeVerifiers need only the signing keys
Size on every requestA short IDGrows with the claims

Follow an anonymous browser through login and logout

Before login, the browser may hold anonymous session A for a shopping cart. After successful authentication, issue a fresh authenticated identifier B and invalidate A’s ability to acquire the new privilege. Otherwise an attacker who knows or planted A can continue using it after the victim logs in. Move only explicitly permitted anonymous state, such as validated cart contents, into the new session.

Set the session cookie as __Host-session=<id>; Path=/; Secure; HttpOnly; SameSite=Lax. The __Host- prefix makes the browser refuse the cookie unless it is Secure, has Path=/ and has no Domain attribute, so a sibling subdomain cannot plant or overwrite it. These attributes reduce particular delivery or script-access risks; they do not replace authorization or CSRF defenses. HttpOnly stops ordinary script reads of the cookie but does not prevent injected script from issuing requests as the user.

Use both idle and absolute timeouts. Idle expiry limits inactivity; absolute expiry caps a session even if an attacker keeps it active. Store or derive the relevant timestamps on trusted infrastructure, not in the cookie expiry, which the client controls. Define the boundary precisely: if expiresAt is 10:30:00, a request at that instant is expired under the exercise policy. Renewing an idle timer must not reset the absolute lifetime.

After password reset, credential compromise, account disablement, or role reduction, decide which sessions and refresh grants must be revoked. A server-side session version is one technique: requests compare their issued version with the account’s current version. Distributed caches need a bounded propagation delay, otherwise “logout everywhere” may be an optimistic label.

Logout invalidates server authority and clears the browser credential. Clearing only a cookie leaves copied credentials usable. For federated login, the application session and identity-provider session are separate. Local logout may leave the provider signed in; the next login can therefore be seamless unless the chosen flow requires reauthentication. Explain that behavior to users accurately.

How long should a session last?

NIST SP 800-63B-4 (July 2025) ties session limits to the authentication assurance level (AAL) of the login. Choose the level your risk assessment requires, then set values at or below these limits and record them in your security documentation. When either timeout expires the session ends; reauthentication resets both.

How long should a session last?
Assurance levelInactivity timeoutOverall timeoutReauthentication
AAL1Optional30 days or less (recommended)Any AAL1 authenticator.
AAL21 hour or less (recommended)24 hours or less (recommended)After an idle timeout, a password or biometric with the session secret may be enough.
AAL315 minutes or less (recommended)12 hours or less (required)The same as the initial AAL3 authentication.

Issue, check and end a session in Node.js

This module shows the server-side rules in about thirty lines: a 256-bit identifier, the __Host- cookie, rotation at login, both clocks with a now >= expiry boundary, and revocation through an account’s session version or disabled flag. It was tested on Node.js 26, including the exact idle boundary and a session kept active past its absolute lifetime. Use your framework’s session store in production and keep the same checks.

Fixedsessions.mjsJavaScript
import {randomBytes} from 'node:crypto';

const IDLE_MS = 30 * 60_000; // 30 minutes without activity (NIST: 1 hour at most for AAL2)
const ABSOLUTE_MS = 12 * 3_600_000; // 12 hours after login, whatever the activity
const sessions = new Map(); // id -> {userId, version, createdAt, lastSeen}; use a shared store in production

// Call after a successful login or reauthentication. Never promote the pre-login identifier.
export function startSession(res, user, oldId, now = Date.now()) {
  if (oldId) sessions.delete(oldId);
  const id = randomBytes(32).toString('base64url'); // 256 bits from the CSPRNG
  sessions.set(id, {userId: user.id, version: user.sessionVersion, createdAt: now, lastSeen: now});
  res.setHeader('Set-Cookie',
    `__Host-session=${id}; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=${ABSOLUTE_MS / 1000}`);
  return id;
}

// Every request: both clocks, an enabled account and the account's current session version.
export function currentSession(id, accounts, now = Date.now()) {
  const s = sessions.get(id);
  const user = s && accounts.get(s.userId);
  const expired = !s || now >= s.lastSeen + IDLE_MS || now >= s.createdAt + ABSOLUTE_MS;
  if (expired || !user || user.disabled || s.version !== user.sessionVersion) {
    sessions.delete(id);
    return null;
  }
  s.lastSeen = now; // activity extends the idle clock, never the absolute one
  return user;
}

export function endSession(res, id) {
  sessions.delete(id);
  res.setHeader('Set-Cookie', '__Host-session=; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=0');
}

Configure sessions in Spring Boot and Spring Security

Spring Boot sets the cookie and the idle timeout from properties: server.servlet.session.cookie.name=__Host-JSESSIONID, server.servlet.session.cookie.secure=true, server.servlet.session.cookie.http-only=true, server.servlet.session.cookie.same-site=lax, server.servlet.session.timeout=30m and server.servlet.session.tracking-modes=cookie, which keeps session IDs out of URLs. Leave server.servlet.session.cookie.domain unset so the __Host- prefix is accepted. Spring Security changes the session ID at sign-in by default; nothing in Spring ends a busy session, so the filter below adds an 8-hour absolute timeout.

This is the code of the session fixation fix card at /fix/session-cookies/java-spring/, tested there with Spring Boot 4.1.1 on JDK 21: the pre-login ID stops working after sign-in, the cookie carries all four attributes, and a session kept busy past 8 hours ends.

FixedSessionConfig.javaJava
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.context.SecurityContextHolderFilter;
import org.springframework.web.filter.OncePerRequestFilter;

@Configuration
class SessionConfig {
    static final Duration ABSOLUTE_TIMEOUT = Duration.ofHours(8);
    static final String SIGNED_IN_AT = "signedInAt";

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http, Clock clock) throws Exception {
        http
                .authorizeHttpRequests(requests -> requests.requestMatchers("/login").permitAll().anyRequest().authenticated())
                .formLogin(form -> form.loginPage("/login").successHandler(recordSignInTime(clock)))
                // The default, shown for clarity: sign-in calls HttpServletRequest.changeSessionId(),
                // so the ID the browser held before sign-in stops working.
                .sessionManagement(session -> session.sessionFixation(fixation -> fixation.changeSessionId()))
                .addFilterBefore(new AbsoluteTimeout(clock), SecurityContextHolderFilter.class);
        return http.build();
    }

    private static AuthenticationSuccessHandler recordSignInTime(Clock clock) {
        SimpleUrlAuthenticationSuccessHandler redirect = new SimpleUrlAuthenticationSuccessHandler("/account");
        return (request, response, authentication) -> {
            request.getSession().setAttribute(SIGNED_IN_AT, clock.instant());
            redirect.onAuthenticationSuccess(request, response, authentication);
        };
    }

    /** The idle timeout comes from server.servlet.session.timeout; this ends a busy session too. */
    static final class AbsoluteTimeout extends OncePerRequestFilter {
        private final Clock clock;

        AbsoluteTimeout(Clock clock) {
            this.clock = clock;
        }

        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
                throws ServletException, IOException {
            HttpSession session = request.getSession(false);
            Instant signedInAt = session == null ? null : (Instant) session.getAttribute(SIGNED_IN_AT);
            if (signedInAt != null && !clock.instant().isBefore(signedInAt.plus(ABSOLUTE_TIMEOUT))) {
                session.invalidate();
            }
            chain.doFilter(request, response);
        }
    }
}

Configure cookie authentication in ASP.NET Core

ASP.NET Core's cookie authentication keeps the signed-in user in an encrypted ticket and issues a new cookie value at every sign-in, which closes session fixation. Sliding expiration gives an idle timeout but renews a busy ticket forever, so OnValidatePrincipal adds the absolute limit. Keep sign-in state out of ISession, which cannot change its ID. For revocation after a password reset with ASP.NET Core Identity, its security stamp validator rejects tickets issued before the stamp changed.

This is the code of the fix card at /fix/session-cookies/csharp-aspnetcore/, tested there on .NET 10: the cookie is __Host-auth with Secure, HttpOnly and SameSite=Lax, a cookie from before sign-in grants nothing afterwards, and a ticket older than 8 hours is rejected.

FixedSessions.csC#
using System.Globalization;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;

public static class Sessions
{
    private const string SignedInAt = "signed_in_at";
    private static readonly TimeSpan AbsoluteTimeout = TimeSpan.FromHours(8);

    public static void AddSessions(this IServiceCollection services) =>
        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddCookie(options =>
            {
                options.Cookie.Name = "__Host-auth"; // browsers accept it only with Secure, Path=/ and no Domain
                options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
                options.Cookie.HttpOnly = true;
                options.Cookie.SameSite = SameSiteMode.Lax;
                // Idle timeout: 30 minutes; a request in the second half of the window renews the ticket.
                options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
                options.SlidingExpiration = true;
                options.LoginPath = "/login";
                // Absolute timeout: sliding expiration alone keeps a busy ticket alive forever.
                options.Events.OnValidatePrincipal = async context =>
                {
                    var now = context.HttpContext.RequestServices.GetRequiredService<TimeProvider>().GetUtcNow();
                    var signedInAt = context.Properties.GetString(SignedInAt);
                    if (signedInAt is null || now - DateTimeOffset.Parse(signedInAt, CultureInfo.InvariantCulture) >= AbsoluteTimeout)
                    {
                        context.RejectPrincipal();
                        await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
                    }
                };
            });

    // After the password (and any second factor) has been verified.
    public static async Task SignInAsync(HttpContext context, string username)
    {
        var identity = new ClaimsIdentity([new Claim(ClaimTypes.Name, username)], CookieAuthenticationDefaults.AuthenticationScheme);
        var properties = new AuthenticationProperties();
        var now = context.RequestServices.GetRequiredService<TimeProvider>().GetUtcNow();
        properties.SetString(SignedInAt, now.ToString("O", CultureInfo.InvariantCulture));
        // A new encrypted ticket in a new cookie value: nothing the browser held before sign-in
        // identifies the signed-in session.
        await context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity), properties);
    }
}

Exercise a state machine, including a stolen old identifier

Create tests for every privilege transition, not just correct and incorrect passwords. A successful login must rotate the session; the old anonymous ID must not retrieve authenticated data. Completing one MFA step must not grant a fully authenticated session before the second step is verified.

At logout, a copied cookie must fail when replayed. At idle and absolute expiry boundaries, protected requests must fail or enter the documented reauthentication flow. Test an account disabled during an active session and a password reset while another device is logged in. These cases reveal revocation gaps that a login-form test cannot.

Operationally, authentication services need rate limiting, monitoring, and safe error handling. Limits should constrain guessing without allowing a trivial denial of service against arbitrary accounts. Recovery and device changes require their own controls; a strong login path cannot compensate for a weak account-recovery path.

Defend against stolen session cookies

Infostealer malware copies session cookies from the browser profile, and the copy works from another machine without the password or the second factor, because MFA happened before the session existed. Short absolute lifetimes, reauthentication for sensitive actions and revoking sessions on suspicious changes of device or network all shrink the window. The Okta support breach (/domains/identity-crypto/authentication/case-study-okta-har-2023/) shows the same replay with tokens copied from HAR files.

Device Bound Session Credentials (DBSC), a W3C Web Application Security Working Group draft, binds a session to a key held in the device’s TPM. After login the server sends a Secure-Session-Registration header; the browser registers a public key, and the server then issues short-lived cookies that the browser refreshes by proving possession of the private key. Chrome 146 made DBSC publicly available on Windows in April 2026, and Google Workspace turned it on for Chrome on Windows in May 2026. Keep enforcing your normal timeouts: NIST SP 800-63B-4 allows device-bound session secrets to persist but still requires the session lifetime limits.

Guided practice

A security review of https://app.example.com collected the five Set-Cookie headers below from different parts of the application, plus a session timeline. Decide what a browser does with each header and whether it is safe for a session, then write the correct header and the tests. Work it out before reading the answers.

Supplied scenario material
Plain text
S1  Set-Cookie: __Host-session=Zm9vYmFy; Path=/; Secure; HttpOnly; SameSite=Lax
S2  Set-Cookie: __Host-session=Zm9vYmFy; Domain=example.com; Path=/; Secure; HttpOnly
S3  Set-Cookie: __Host-session=Zm9vYmFy; Path=/app; Secure; HttpOnly
S4  Set-Cookie: session=Zm9vYmFy; Path=/; HttpOnly; SameSite=None
S5  Set-Cookie: sid=Zm9vYmFy; Path=/; Secure; SameSite=Strict; Max-Age=2592000
Timeline: anonymous ID A issued 09:00; password and MFA succeed 09:05 and the server keeps ID A;
idle timeout 15 minutes, absolute timeout 60 minutes, reject at now >= expiry; last activity 09:20;
password reset on another device at 09:30 increments the account session version from 4 to 5.
  1. 1. Decide what the browser stores

    For S1 to S5, say whether a current browser stores the cookie, and why.

    Expected result and reasoning

    S1 is stored. S2 is ignored: a __Host- cookie must not have a Domain attribute. S3 is ignored: a __Host- cookie must have Path=/. S4 is ignored by browsers that follow RFC 6265bis, including Chromium-based ones, because SameSite=None requires Secure. S5 is stored.

  2. 2. Judge the stored ones

    Say what is wrong, if anything, with S1 and S5 as session cookies.

    Expected result and reasoning

    S1 is right: __Host- binds it to this host and HTTPS, HttpOnly keeps it from script, and SameSite=Lax limits cross-site sends. S5 has no HttpOnly, so any injected script can read the session ID, and Max-Age=2592000 makes it a 30-day persistent cookie that survives browser restarts, longer than any timeout the server enforces should allow; it also lacks the __Host- prefix, so a sibling subdomain could overwrite it.

  3. 3. Find the fixation bug

    Read the timeline and name the flaw before 09:30.

    Expected result and reasoning

    The server kept ID A after authentication at 09:05, so anyone who knew or planted A before login now holds an authenticated session: session fixation. Issue a new ID at login (and at every privilege change), and invalidate A.

  4. 4. Evaluate the clocks and the reset

    Decide whether the session works at 09:31, at 09:34:59 and 09:35:00 without the reset, and after 10:05 with continuous activity.

    Expected result and reasoning

    At 09:31 it fails: the reset moved the account to version 5 and the session carries 4. Ignoring the reset, 09:34:59 is within the idle window (last activity 09:20 plus 15 minutes) and 09:35:00 is expired under now >= expiry. With continuous activity it still ends at 10:05, 60 minutes after the 09:05 login, because activity never extends the absolute timeout.

  5. 5. Write the header and the tests

    Write the Set-Cookie header your login should send and the tests that prove the session rules.

    Expected result and reasoning

    Set-Cookie: __Host-session=<new 128-bit-or-more random ID>; Path=/; Secure; HttpOnly; SameSite=Lax, with no Max-Age so the cookie ends with the browser while the server enforces the timeouts. Tests: the pre-login ID fails after login; the header has all four attributes and the prefix; a request at exactly the idle expiry fails; a constantly active session fails at the absolute limit; a password reset ends other devices' sessions on their next request; and a copied cookie fails after logout.

Your deliverable: A table of S1 to S5 (stored? safe? why), the corrected header, the fixation fix and a list of six tests.

The headers are examples; browsers differ in details such as how they treat cookies set over plain HTTP. Check your own responses with browser developer tools and an automated test against the running app.

Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.

No draft saved on this device yet.

Sources & further reading

The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.

  1. OWASP Session Management Cheat Sheet
  2. OWASP Authentication Cheat Sheet
  3. NIST SP 800-63B-4: session management and reauthentication. SP 800-63B-4 (July 2025).
  4. OWASP ASVS 5.0: V7 Session Management. Version 5.0.0 (May 2025).
  5. Chrome for Developers: Device Bound Session Credentials
  6. Google: protecting cookies with Device Bound Session Credentials (April 2026)
  7. IETF draft RFC 6265bis: cookie prefixes and SameSite storage rules. draft-ietf-httpbis-rfc6265bis-22.
  8. Spring Boot: servlet session properties
  9. Microsoft: use cookie authentication without ASP.NET Core Identity

Reference information

SDLC phase
Development
ASVS 5.0.0 requirements
v5.0.0-7.2.3 · v5.0.0-7.2.4 · v5.0.0-7.3.1 · v5.0.0-7.3.2 · v5.0.0-7.4.1 · v5.0.0-7.4.2 · v5.0.0-7.5.1 · v5.0.0-3.3.1 · v5.0.0-3.3.3

Download this guide as Markdown