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=LaxorStrict,Path=/and noDomainattribute. - 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.
| Setting | Value | Note | Source |
|---|---|---|---|
| Session identifier | At least 128 bits from a CSPRNG | NIST’s floor is 64 bits; 32 random bytes is a common choice. | ASVS 5.0 V7.2.3; NIST SP 800-63B-4 section 5.1 |
| Cookie | __Host- prefix, Secure, HttpOnly, SameSite=Lax or Strict, Path=/ | No Domain attribute; an opaque value with no personal data. | NIST SP 800-63B-4 section 5.1.1; ASVS 5.0 V3.3 |
| Idle timeout | AAL2: 1 hour or less; AAL3: 15 minutes or less | OWASP: 2 to 5 minutes for high-value applications, 15 to 30 minutes for low-risk ones. | NIST SP 800-63B-4 sections 2.2.3 and 2.3.3; OWASP Session Management Cheat Sheet |
| Absolute timeout | AAL1: 30 days; AAL2: 24 hours; AAL3: 12 hours (maximums) | OWASP suggests 4 to 8 hours for an application used through a working day. | NIST SP 800-63B-4 section 2; OWASP Session Management Cheat Sheet |
| Rotation | New identifier on login, reauthentication and privilege change | Terminate the previous identifier at the same moment. | ASVS 5.0 V7.2.4 |
| Sensitive account changes | Full reauthentication before changing email, phone or MFA | A valid old session is not enough to add an authenticator. | ASVS 5.0 V7.5.1 |
| Termination | End all sessions when an account is disabled; offer to end others after a credential change | Includes refresh grants held by API clients. | ASVS 5.0 V7.4.2 and V7.4.3 |
- Log in and confirm the
Set-Cookieheader carries the__Host-prefix,Secure,HttpOnly,SameSiteandPath=/, and a new identifier. - Replay the pre-login cookie after login, and a copied cookie after logout: both must fail.
- Leave a session idle until the timeout and keep another active past the absolute limit: both must require login.
- Reset the password on one device and confirm sessions on other devices stop working on their next request.
- 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.
- Part of
- Workflows: Fix a finding, Secure a feature, Test an application
Tested fixes: the vulnerable code, the fix, a plausible fix that still fails, and the regression tests that tell them apart.
Learning goals and prerequisites
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.
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.
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.
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.
| Assurance level | Inactivity timeout | Overall timeout | Reauthentication |
|---|---|---|---|
| AAL1 | Optional | 30 days or less (recommended) | Any AAL1 authenticator. |
| AAL2 | 1 hour or less (recommended) | 24 hours or less (recommended) | After an idle timeout, a password or biometric with the session secret may be enough. |
| AAL3 | 15 minutes or less (recommended) | 12 hours or less (required) | The same as the initial AAL3 authentication. |
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.
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');
}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.
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);
}
}
}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.
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);
}
}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.
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
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. 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 aDomainattribute. S3 is ignored: a__Host-cookie must havePath=/. S4 is ignored by browsers that follow RFC 6265bis, including Chromium-based ones, becauseSameSite=NonerequiresSecure. S5 is stored.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,HttpOnlykeeps it from script, andSameSite=Laxlimits cross-site sends. S5 has noHttpOnly, so any injected script can read the session ID, andMax-Age=2592000makes 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. 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. 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. Write the header and the tests
Write the
Set-Cookieheader 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 noMax-Ageso 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.
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.
- OWASP Session Management Cheat Sheet
- OWASP Authentication Cheat Sheet
- NIST SP 800-63B-4: session management and reauthentication. SP 800-63B-4 (July 2025).
- OWASP ASVS 5.0: V7 Session Management. Version 5.0.0 (May 2025).
- Chrome for Developers: Device Bound Session Credentials
- Google: protecting cookies with Device Bound Session Credentials (April 2026)
- IETF draft RFC 6265bis: cookie prefixes and SameSite storage rules. draft-ietf-httpbis-rfc6265bis-22.
- Spring Boot: servlet session properties
- Microsoft: use cookie authentication without ASP.NET Core Identity
Reference information
- SDLC phase
- Development
- Role
- Developers
- 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
- CAPEC attack patterns