Session Management
Session Management
Session management is the set of mechanisms by which an application maintains authenticated state across HTTP requests — either by issuing a server-side session identifier (opaque cookie referencing stored state) or a stateless token (self-contained JWT the client presents on each request) — with each model presenting distinct security trade-offs.
Core Idea
HTTP is stateless — every request is independent and carries no memory of previous interactions. After a user authenticates, the application must issue a credential that proves "this request comes from the user who already authenticated." Two fundamentally different approaches exist:
-
Server-side session: the server stores session data (user ID, roles, expiry) in memory or a distributed store (Redis); the client receives an opaque session ID in a cookie; the server looks up the session ID on every request. Revocation is instant — delete the session. State can be mutable (update roles without reissuing token).
-
Stateless JWT session: the server issues a signed JWT containing user identity and claims; the client stores the JWT (cookie or Authorization header) and presents it on every request; the server validates the signature locally — no store lookup needed. Scales horizontally without a shared store. Revocation requires waiting for token expiry or maintaining a denylist.
The BFF-for-SPA pattern uses server-side sessions — the OAuth2-BFF-Pattern stores tokens in Redis and issues an opaque httpOnly session cookie to the browser. Stateless JWT sessions are common in API-only services and mobile app backends.
When NOT to Use
When NOT to Use Server-Side Sessions
- Do not use server-side sessions for stateless microservices that must scale to many instances without a shared store — without Redis or equivalent, sessions are pinned to a single instance; sticky sessions mask a horizontal scaling failure
- Do not use server-side sessions for public APIs consumed by non-browser clients — mobile apps and server-to-server API clients are better served by OAuth2 access tokens (JWTs)
- Do not use in-memory session storage in production — in-memory sessions are lost on restart and cannot be shared across instances; always use an external store (Redis) in production
When NOT to Use Stateless JWT Sessions
- Do not use long-lived JWTs (hours or days) for user sessions without a revocation strategy — a stolen JWT is valid until expiry; immediate revocation is impossible without a denylist
- Do not store sensitive, mutable data in JWT claims — if the user's roles change, the JWT still carries the old roles until it expires; for frequently-changing authorization state, prefer server-side sessions or very short JWT expiry
- Do not implement JWT sessions with HS256 in distributed systems — shared secret means any service that validates can also forge; see JWT for algorithm guidance
How It Works
Cookie Security Attributes
All three attributes are required for production session cookies:
| Attribute | Value(s) | Threat mitigated | Behavior |
|---|---|---|---|
httpOnly | (flag, no value) | XSS token theft | Cookie is inaccessible to JavaScript (document.cookie returns nothing for httpOnly cookies); XSS attacks cannot read the session ID |
Secure | (flag, no value) | Network interception (MITM) | Cookie is only transmitted over HTTPS connections; never sent over plain HTTP |
SameSite | Strict / Lax / None | CSRF | Controls whether the cookie is sent on cross-site requests; restricts cross-site cookie transmission |
SameSite Values
SameSite=Strict: cookie sent only on same-site requests — full CSRF protection, but breaks OAuth2 redirect flows (the redirect back from the IdP is a cross-site top-level navigation, whichStrictblocks); avoid for OAuth2 redirect-based authenticationSameSite=Lax(recommended default): cookie sent on same-site requests and top-level cross-site GET requests; safe for OAuth2 redirect flows; provides CSRF protection for all state-changing (non-GET) requestsSameSite=None; Secure: cookie sent on all cross-site requests; required for cross-site widgets and iframes; must be paired with theSecureattribute
The recommended production combination: Set-Cookie: SESSION=...; httpOnly; Secure; SameSite=Lax
For the BFF-for-SPA configuration where Spring Security sets these attributes automatically, see BFF-For-SPA.
Session Fixation and Session Rotation
Session Fixation (attack)
An attacker pre-sets a known session ID for a victim's browser (e.g., via URL parameter ?JSESSIONID=attackerKnownId). The victim authenticates. If the application does not issue a new session ID after authentication, the attacker's known session ID is now a valid authenticated session — the attacker is effectively logged in as the victim without knowing the victim's credentials.
Mitigation: Session Rotation
Always issue a new session ID immediately after successful authentication. The pre-authentication session (used for CSRF state and PKCE verifier storage) is invalidated; a new session is created for the authenticated state. This makes the attacker's pre-set session ID worthless — it is destroyed on login.
Spring Security performs session fixation protection (session rotation) automatically on authentication by default via SessionFixationProtectionStrategy.
Session Rotation on Refresh (Stateless JWT)
For stateless JWT sessions, rotate refresh tokens on every use (refresh token rotation). A stolen refresh token used by an attacker invalidates the refresh token in the server's denylist — the legitimate user's next refresh request fails, alerting them of the compromise. Without rotation, a stolen refresh token is valid indefinitely.
Server-Side vs Stateless JWT Decision
| Dimension | Server-Side Session | Stateless JWT Session |
|---|---|---|
| Storage | External store (Redis) required | Client-side (cookie or Authorization header) |
| Revocation | Instant (delete from store) | Deferred (wait for expiry) or denylist |
| Scalability | Requires shared session store | Scales horizontally without shared store |
| Mutable state | Supported — update session data anytime | Not supported — claims are fixed at issuance |
| Token introspection | Session lookup on every request | Local signature validation (no store lookup) |
| Best for | Browser SPAs (BFF pattern), mutable roles | API services, mobile apps, microservices |
Mermaid State Diagram: Session Lifecycle
stateDiagram-v2
[*] --> Unauthenticated
Unauthenticated --> PreAuthSession : User initiates login\n(session created for CSRF/PKCE state)
PreAuthSession --> Authenticated : Authentication succeeds\nNEW session ID issued (rotation)
Authenticated --> Active : Session cookie set\nhttpOnly + Secure + SameSite=Lax
Active --> Active : Request with valid session\nSliding window resets timeout
Active --> Expired : Absolute timeout reached\nor sliding window idle timeout
Active --> Invalidated : Logout or forced\nrevocation
Expired --> [*]
Invalidated --> [*]
The critical transition is PreAuthSession → Authenticated: a new session ID is issued at this point, destroying the pre-authentication session. This is session rotation — the mitigation for session fixation attacks.
TypeScript Example
// Session configuration — httpOnly + Secure + SameSite=Lax is the production minimum
import session from 'express-session'
import RedisStore from 'connect-redis'
import { createClient } from 'redis'
const redisClient = createClient({ url: process.env.REDIS_URL })
await redisClient.connect()
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET!, // use a strong random secret — never a hardcoded string
name: '__Host-session', // __Host- prefix enforces Secure + path=/ + no Domain attribute
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true, // blocks XSS token theft
secure: true, // HTTPS only
sameSite: 'lax', // CSRF protection + OAuth2 redirect compatibility
maxAge: 30 * 60 * 1000, // 30 minutes in ms
},
}))
// Session rotation after authentication — regenerate ID to prevent session fixation
app.post('/auth/callback', (req, res) => {
req.session.regenerate((err) => { // new session ID, old session invalidated
if (err) return res.status(500).send('Session error')
req.session.userId = authenticatedUser.id
res.redirect('/')
})
})The __Host- cookie prefix is a browser security enhancement that enforces three constraints simultaneously: the cookie must be sent over HTTPS (Secure attribute required), the cookie path must be / (no path-scoped cookie bypasses), and no Domain attribute is allowed (cookie is bound to the exact origin, not subdomains).
For stateless JWT sessions, configure the cookie the same way — but the cookie value is the JWT itself (or a short-lived reference token pointing to a server-side denylist). See JWT for JWT validation mechanics.
Java Example
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
@Configuration
public class SessionConfig {
// Spring Security performs session fixation protection (session rotation)
// automatically on authentication by default via SessionFixationProtectionStrategy
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("__Host-SESSION"); // __Host- prefix enforces Secure+path=/ (no domain attr)
serializer.setUseHttpOnlyCookie(true); // blocks XSS token theft
serializer.setUseSecureCookie(true); // HTTPS only
serializer.setSameSite("Lax"); // CSRF protection + OAuth2 redirect compatibility
serializer.setCookieMaxAge(30 * 60); // 30 minutes in seconds
return serializer;
}
}For full BFF session configuration with Redis, OAuth2 token storage, and Spring Security filter chain, see OAuth2-BFF-Pattern. For the complete end-to-end SPA session flow including token storage in Redis and httpOnly cookie setup, see BFF-For-SPA.
Related Concepts
| Concept | Relationship |
|---|---|
| JWT | Stateless session token option — must be validated (signature before claims) on every request |
| OAuth2-OIDC-Flows | OAuth2 authentication produces the session context this note manages |
| OAuth2-BFF-Pattern | BFF pattern uses server-side Redis session with httpOnly cookie; Redis configuration YAML belongs there |
| BFF-For-SPA | End-to-end SPA flow demonstrating the httpOnly session cookie lifecycle (10-step flow) |
| RBAC-ABAC | Authorization decisions are made within the authenticated session context |