How do you implement stateless JWT-based authentication in Spring Security?

9 minadvancedjwtstateless-authenticationsecurity

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 is 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.STATELESS tells Spring Security to never create or use an HttpSession at 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.