Session Management: Timeout, Concurrency & Fixation
Session-based auth has its own failure modes that token-based auth simply doesn't share. This chapter is timeout, concurrent-session limits, and fixation attacks — and why REST APIs usually skip sessions entirely.
Learning objectives
- Beginner: Explain what a session fixation attack is and why it matters.
- Intermediate: Configure session timeout and concurrent-session limits for a traditional web application.
- Advanced: Justify why a stateless REST API disables sessions entirely rather than tuning session settings.
🌱 BEGINNER STORY
When you check into a hotel, you get a key card programmed to open your room for exactly the duration of your stay (a SESSION). If you don't use the card for a long time, some hotels deactivate it as a safety measure (SESSION TIMEOUT). A well-run hotel also won't let the same key card be freely copied and used by five different people simultaneously in the same room (CONCURRENT SESSION CONTROL). And critically, the front desk reissues you a brand NEW key card the moment you actually check in — they never let you keep using a card that was handed out to 'whoever showed up at the counter' before your identity was verified (SESSION FIXATION PROTECTION) — otherwise someone could hand you a pre-made card, then use a duplicate of that same card to walk into your room after you've checked in.
An HttpSession that's inactive for too long is a security AND resource risk (idle sessions pile up, and a device left unattended stays 'logged in' indefinitely). Configure this in application.properties or via session management settings:
server.servlet.session.timeout=15m
http.sessionManagement(session -> session .invalidSessionUrl("/login?invalid") .maximumSessions(1) .maxSessionsPreventsLogin(false));
invalidSessionUrl handles the case where a client presents a session ID that the server no longer recognizes (already expired, or server restarted and lost in-memory sessions) — instead of a confusing generic error, the user is redirected somewhere sensible.
maximumSessions(1) means a user can only have ONE active session at a time — logging in from a second device/browser will either block the new login (maxSessionsPreventsLogin(true)) or silently invalidate the OLDER session (maxSessionsPreventsLogin(false), the default) so only the newest login remains valid. Banking apps commonly use this to prevent an account being simultaneously active on a stolen device and the legitimate owner's device.
⚠ A subtle but important gotcha
Concurrent session control REQUIRES registering an HttpSessionEventPublisher as a servlet listener — without it, Spring Security's internal SessionRegistry never finds out when a session is destroyed (e.g., on logout), and it will incorrectly believe old sessions are still active, eventually locking legitimate users out even though they only have one real active session.
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() { return new HttpSessionEventPublisher(); }
Figure: Session fixation: an attacker plants a known session ID, then hijacks it once the victim authenticates under that same ID.
A session fixation attack works like this: an attacker obtains a valid (but unauthenticated) session ID from the target website — often by simply visiting it themselves — and tricks the victim into using that EXACT session ID (e.g., via a crafted link containing a jsessionid parameter, on older/misconfigured servers). If the server naively upgrades that SAME session to an authenticated state after the victim logs in — rather than issuing a brand-new session ID — the attacker, who already knows that session ID, is now instantly logged in as the victim too.
Spring Security defends against this by default: on every successful authentication, it creates a NEW session ID (and optionally a new session entirely), invalidating whatever session ID existed before login. This is configurable:
http.sessionManagement(session -> session
.sessionFixation(fixation -> fixation.newSession()) // options: newSession() | migrateSession() | changeSessionId() (default) | none() );
| Strategy | Behavior |
|---|---|
| changeSessionId() (default) | Keeps the same HttpSession object but changes its ID — efficient, works well in most servlet containers. |
| migrateSession() | Creates a brand-new session AND copies over existing session attributes. |
| newSession() | Creates a brand-new, completely empty session — most secure but loses any pre-login session data. |
| none() | Disables protection entirely — never use this in production. |
For token-based (JWT/OAuth2) REST APIs, there is often no server-side session at all — each request is verified independently using the token's signature. This is configured with:
http.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
STATELESS tells Spring Security to never create or use an HttpSession for authentication state — every request must carry its own proof of identity (typically a JWT bearer token). This is the standard approach for microservices and mobile-app backends, since it removes the need for sticky sessions or a shared session store across horizontally-scaled server instances.
⚠ Where teams get tripped up
Load-balanced deployments with multiple server instances and STATEFUL sessions require either sticky sessions (routing a user always to the same instance) or a shared/distributed session store (like Redis via Spring Session) — otherwise a user can randomly get logged out when the load balancer routes them to a different instance that doesn't have their session. Setting an extremely long session timeout 'for convenience' significantly widens the window during which a stolen session cookie remains useful to an attacker. Forgetting HttpSessionEventPublisher makes maximumSessions() appear to work in testing (fresh server, few sessions) but fail mysteriously in production once users log out and back in repeatedly. Mixing STATEFUL session-based auth for the web UI and STATELESS JWT auth for a separate mobile API in the SAME application requires two distinct SecurityFilterChain beans with different session policies — trying to force one config to do both usually breaks one of the two clients.
Want a visual for this concept?
Generate a diagram tailored to “Session Management: Timeout, Concurrency & Fixation” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →