How do you implement stateless JWT-based authentication in Spring Security?
Quick Answer
Stateless JWT auth means the server holds no session state at all — the client sends a signed JWT (typically in an Authorization: Bearer header) on every request, and the server verifies the token's signature and expiration to establish identity for that single request, with no server-side session lookup. In Spring Security, this is implemented with a custom OncePerRequestFilter that parses and validates the incoming token, populates the SecurityContext with an Authentication object derived from its claims, and a SecurityFilterChain configured with .sessionCreationPolicy(SessionCreationPolicy.STATELESS) so Spring Security never creates or relies on an HTTP session.
Detailed Answer
Traditional session-based authentication stores authenticated state server-side (in an HTTP session), identified by a session cookie — every request requires a server-side session lookup, and scaling horizontally requires either sticky sessions or a shared session store.
Stateless JWT authentication avoids server-side session state entirely: after a successful login, the server issues a signed JSON Web Token containing the user's identity (and typically their roles/claims) directly inside the token itself. The client sends this token on every subsequent request (conventionally in an Authorization: Bearer <token> header), and the server verifies the token's signature and expiration on each request — no session lookup needed, since the token itself carries everything required to establish identity for that single request.
Implementing this in Spring Security:
1. A custom filter that parses and validates the incoming token, and populates the SecurityContext if valid:
class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String token = extractTokenFromHeader(request);
if (token != null && jwtService.isValid(token)) {
Authentication auth = jwtService.getAuthentication(token); // derived from the token's claims
SecurityContextHolder.getContext().setAuthentication(auth);
}
chain.doFilter(request, response);
}
}
2. Register the filter and disable session-based state in the SecurityFilterChain:
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtFilter) throws Exception {
http
.csrf(csrf -> csrf.disable()) // CSRF protection is primarily for cookie/session-based auth; not needed for bearer tokens
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/login").permitAll()
.anyRequest().authenticated())
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
Important practical considerations:
SessionCreationPolicy.STATELESStells Spring Security to never create or use anHttpSessionat all — every request must carry (and be re-authenticated from) its own token.- Token expiration and revocation: since the server doesn't track issued tokens, a compromised or logged-out token normally remains valid until it expires — genuine "revoke this token right now" support requires an additional mechanism (a server-side blocklist of revoked token IDs, or very short-lived access tokens paired with a separately-revocable refresh token).
- Signature verification (typically HMAC with a shared secret, or RSA/EC with a public/private key pair for services that need to verify tokens without holding the signing secret) is what actually prevents a client from forging or tampering with a token's claims — the server must always verify the signature, never simply trust and decode an unsigned or unverified token.