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 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.