You built a STATELESS JWT API but SecurityContextHolder.setAuthentication() from a controller doesn't stick across requests, and a JSESSIONID cookie still shows up. Diagnose and design the correct approach.
answer
- STATELESS -> context not persisted, thread-local cleared per request
- re-auth every request via token filter (OncePerRequestFilter / jwt resource server)
- JSESSIONID culprit: CSRF session repo / MVC / getSession()
- disable CSRF for bearer APIs; audit getSession
- assert no Set-Cookie JSESSIONID in a test
basics
~20 sSTATELESS means Spring Security never stores the context in a session, so setting it in a controller can't persist — you must re-establish auth every request via a token filter. A JSESSIONID appears because other code (CSRF, MVC, request.getSession) still creates sessions.
solid answer
~40 sTwo separate issues. (1) Nothing persists across requests because SessionCreationPolicy.STATELESS wires a non-session SecurityContextRepository; the SecurityContextHolder is a per-request thread-local cleared at request end. Programmatically setting an Authentication in a controller only lasts that request. The correct design is a per-request authentication filter (e.g. OncePerRequestFilter or oauth2ResourceServer().jwt()) that validates the token and populates the context on every call. (2) A JSESSIONID still appears because STATELESS only stops Spring Security from using sessions — CSRF's default server-side token repository, Spring MVC flash attributes, or a stray request.getSession() can still create one. Disable/ignore CSRF for the token API (or use a stateless CSRF strategy), avoid getSession(), and if you must, set the servlet container to not create sessions. Verify with an integration test asserting no Set-Cookie: JSESSIONID.
code
java · 17 lines@Bean
SecurityFilterChain apiChain(HttpSecurity http, JwtDecoder decoder) throws Exception {
http
.securityMatcher("/api/**")
.csrf(csrf -> csrf.disable()) // no server-side CSRF token -> no session
.sessionManagement(s -> s
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(a -> a.anyRequest().authenticated())
// per-request authentication; nothing stored server-side:
.oauth2ResourceServer(o -> o.jwt(j -> j.decoder(decoder)));
return http.build();
}
// Verify statelessness in a test:
// mockMvc.perform(get("/api/me").header("Authorization", "Bearer " + jwt))
// .andExpect(status().isOk())
// .andExpect(header().doesNotExist("Set-Cookie"));go deeper
Recognize STATELESS means re-authenticating each request with a token.
Explain that the context isn't persisted and that a token filter must run per request.
Enumerate the session-creating culprits (CSRF repo, MVC, getSession) and how to silence them.
Design multi-chain layouts, reason about revocation trade-offs, and enforce/verify statelessness at container and test level.
**Symptom 1 — the manually-set Authentication doesn't survive.** - `SecurityContextHolder` stores the `SecurityContext` in a `ThreadLocal` that `SecurityContextHolderFilter` clears at the end of each request. Persistence across requests is the job of the configured `SecurityContextRepository`. - With `SessionCreationPolicy.STATELESS`, `SessionManagementConfigurer` arranges that the context is **not** written to an `HttpSession` (effectively a null/request-scoped persistence). So even calling `SecurityContextHolder.getContext().setAuthentication(auth)` (and even `saveContext`) will not carry to the next request — by design. - **Correct model:** identity must be *re-derived on every request* from a self-contained credential. Add an authentication filter that runs per request: - Use Spring Security's resource server: `http.oauth2ResourceServer(o -> o.jwt(...))`, which registers `BearerTokenAuthenticationFilter` + `JwtAuthenticationProvider` to validate the JWT signature/claims and set the context each request; or - A custom `OncePerRequestFilter` that reads the `Authorization: Bearer` header, validates it, builds an `Authentication`, and sets it on the holder for that request only. - Never try to 'remember' the login server-side in STATELESS mode; that reintroduces the state you chose to avoid. **Symptom 2 — an unexpected JSESSIONID cookie.** `STATELESS` constrains *Spring Security*, not the whole app. Common creators of a session anyway: - **CSRF token storage.** The default `HttpSessionCsrfTokenRepository` stores the CSRF token in the session → a session is created. For a token API you typically `csrf(csrf -> csrf.disable())` (bearer-token APIs are not browser-cookie CSRF targets) or switch to `CookieCsrfTokenRepository`/a stateless strategy. - **Spring MVC** flash attributes / `SessionAttributes` / a controller calling `request.getSession()` or `getSession(true)`. - **JSP/Thymeleaf** or other libraries touching the session. - **Spring Session** misconfiguration. **Hardening for true statelessness.** 1. `sessionCreationPolicy(STATELESS)` on the filter chain. 2. `csrf` disabled or stateless. 3. Audit app code for `request.getSession()`; remove or pass `false`. 4. Optionally enforce at the container: e.g. a servlet setting to forbid session creation, or a filter that wraps the request to reject `getSession(true)`. 5. **Verify with a test:** hit an endpoint and assert the response has no `Set-Cookie: JSESSIONID` and that a second request without the token is rejected (proving no server-side memory). **Design/trade-off reasoning (principal lens).** - Stateless tokens buy horizontal scale (no session affinity, no Redis/session replication) but cost you server-side revocation — you need short TTLs + refresh tokens or a token denylist. - STATELESS makes session-fixation protection and concurrent-session control moot (nothing to fix/limit) — don't waste config on them. - If any endpoint genuinely needs server-side state, isolate it behind a *second* `SecurityFilterChain` with its own policy rather than compromising the stateless API. **Key gotcha to articulate:** STATELESS is a Spring Security policy, not a servlet-container guarantee; proving no session requires also silencing CSRF/MVC/manual session creation, then asserting it in a test.
- Why doesn't disabling CSRF create a security hole for a Bearer-token API?CSRF exploits ambient credentials the browser auto-sends (cookies). A Bearer token is sent explicitly by JS, not attached automatically by the browser, so a cross-site form/img can't forge it. Cookie-based sessions still need CSRF protection; pure token APIs generally don't.
- How do you handle logout/revocation with stateless JWTs?There is no server session to invalidate, so use short access-token TTLs plus refresh tokens, and/or a server-side denylist (jti) checked per request — trading some statelessness for revocation.
- The team needs one stateful endpoint alongside the stateless API. What do you do?Define a second SecurityFilterChain with its own securityMatcher and IF_REQUIRED policy, ordered appropriately, so the stateless API keeps STATELESS while the stateful endpoint gets its own session handling.
saying these in an interview costs you the question
- Claiming STATELESS alone guarantees no JSESSIONID cookie will ever be issued.
- Trying to 'fix' the non-persisting context by storing it in a session, defeating the stateless design.
- Thinking session-fixation or concurrent-session control still need configuring under STATELESS.
- Assuming disabling CSRF is always unsafe, even for pure bearer-token APIs.