Skip to content
AdvancedArchitecture

JWT Authentication: Anatomy of a Filter Chain

The token is only half the story. The half that actually decides whether a request is authenticated is a filter that runs before your controller ever sees the request.

Hen HeangJuly 10, 20269 min read
Spring BootSecurityJWTJava

A JWT is just a signed string with three parts: a header (signing algorithm + token type), a payload (the claims โ€” usually the subject and an expiry), and a signature that lets the server verify the payload hasn't been tampered with. The interesting engineering isn't the token format โ€” it's the filter that runs on every request to check it.

Where the token comes from

AuthTokenFilter.java
private String parseJwt(HttpServletRequest request) {
    String headerAuth = request.getHeader("Authorization");
    if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
        return headerAuth.substring(7, headerAuth.length());
    }
    return null;
}

Tip

That substring(7) is skipping past the literal string "Bearer " (7 characters, including the trailing space) to get to the raw token. If you've ever wondered exactly what that magic number means in an Authorization header โ€” that's it.

The filter that runs before your controller

AuthTokenFilter.java
public class AuthTokenFilter extends OncePerRequestFilter {

    @Autowired private JwtUtils jwtUtils;
    @Autowired private UserService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                     FilterChain filterChain) throws ServletException, IOException {
        try {
            String jwt = parseJwt(request);
            if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
                String username = jwtUtils.getUserNameFromJwtToken(jwt);
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);

                UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception e) {
            logger.error("Cannot set user authentication: {}", e);
        }
        filterChain.doFilter(request, response);
    }
}

This is the piece that actually answers the question I explained in [[prompt-engineering-for-backend-developers]]'s worked example: authentication ends up non-null in SecurityContextHolder only if this filter ran, found a valid token, and populated it. No valid token, no exception thrown either โ€” the filter just doesn't call setAuthentication(), and the request continues unauthenticated. That's why a downstream null check on authentication is meaningful: it's not defensive paranoia, it's reading the direct result of this filter's decision.

Wiring the filter into the security chain

WebSecurityConfig.java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.cors().and().csrf().disable()
        .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
        .authorizeHttpRequests((requests) -> requests
            .requestMatchers("/", "/auth/login", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
            .requestMatchers("/admin/**").hasAnyAuthority("ADMIN_ROLE")
            .anyRequest().authenticated());
    http.authenticationProvider(authenticationProvider());
    http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
    return http.build();
}
  • STATELESS session policy โ€” the server keeps no session state between requests; every request re-proves identity via the token, which is what makes JWT auth horizontally scalable without sticky sessions.
  • addFilterBefore(..., UsernamePasswordAuthenticationFilter.class) โ€” the JWT filter runs before Spring Security's own username/password filter, so token-based requests are authenticated first in the chain.
  • permitAll() paths are a whitelist, not a suggestion โ€” anything not explicitly listed falls through to .anyRequest().authenticated().

Validating, not just decoding

JwtUtils.java
public boolean validateJwtToken(String authToken) {
    try {
        Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
        return true;
    } catch (SignatureException e) {
        logger.error("Invalid JWT signature: {}", e.getMessage());
    } catch (ExpiredJwtException e) {
        logger.error("JWT token is expired: {}", e.getMessage());
    } catch (MalformedJwtException | UnsupportedJwtException | IllegalArgumentException e) {
        logger.error("Invalid JWT token: {}", e.getMessage());
    }
    return false;
}

Best Practice

parseClaimsJws() re-verifies the signature against the server's secret on every call โ€” it doesn't just decode the base64 payload. A tampered payload with a mismatched signature throws SignatureException here and never reaches the point of setting an Authentication object.