skip to content

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.

level: principalimportance: should knowfreq 30%

answer

  1. STATELESS -> context not persisted, thread-local cleared per request
  2. re-auth every request via token filter (OncePerRequestFilter / jwt resource server)
  3. JSESSIONID culprit: CSRF session repo / MVC / getSession()
  4. disable CSRF for bearer APIs; audit getSession
  5. assert no Set-Cookie JSESSIONID in a test

basics

~20 s

STATELESS 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 s

Two 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
java
@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

for a junior

Recognize STATELESS means re-authenticating each request with a token.

for a middle

Explain that the context isn't persisted and that a token filter must run per request.

for a senior

Enumerate the session-creating culprits (CSRF repo, MVC, getSession) and how to silence them.

for a principal

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.

context