Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 70

Custom JWT Validation Filter

Recommended resources

Welcome back! In our last session, you successfully built a login endpoint that issues a JSON Web Token upon successful authentication. We now have a way for clients to obtain a token, but our protected API endpoints don't yet know how to validate or use it. If you try to access a protected resource like GET /users, the request will still be denied because the server has no memory of the user's login.

This lesson will bridge that gap. We will implement a custom Spring Security filter that intercepts every incoming request. This filter will act as a gatekeeper: it will look for a JWT in the Authorization header, validate it, and if the token is legitimate, it will establish the user's identity for the duration of that request. Your experience with JWT middleware in your MERN stack project will be helpful here; the concept is identical—intercepting a request to perform authentication before it reaches the controller.

By the end of this lesson, you will have a fully functional, stateless authentication system where a valid JWT is the key to accessing protected resources.

The Anatomy of a JWT Request Flow

Before we write any code, let's visualize the process. When a client makes a request to a protected endpoint, it includes the JWT in the Authorization header, typically prefixed with "Bearer ". Our application needs to:

  1. Intercept the request.
  2. Extract the token from the header.
  3. Validate the token's signature and check if it has expired.
  4. Extract the user's identity (username) from the token.
  5. Load the user's details from the database to confirm they exist and get their roles/permissions.
  6. If all checks pass, inform Spring Security that the user is authenticated for this request.
  7. Allow the request to proceed to the controller.

The diagram below provides a detailed illustration of this entire flow, including both success and failure paths.

This diagram shows how a custom `JwtAuthenticationFilter` intercepts an HTTP request. It extracts and validates the JWT, loads user details, and upon success, updates the Security Context before the request proceeds to the controller. Red lines indicate failure paths, such as a missing or invalid JWT.

To implement this, we need to create a custom filter and integrate it into Spring Security's filter chain.


Step 1: Creating the JWT Authentication Filter

The heart of our solution is a custom filter class. Spring Security provides a convenient base class, OncePerRequestFilter, which ensures our filter logic is executed exactly once for each incoming request.

We will create a class named JwtAuthenticationFilter that extends OncePerRequestFilter and implements its core logic in the doFilterInternal method. This video from Teddy Smith provides a great walkthrough of the initial setup.

Spring Boot Security - JWT Filter

This video demonstrates how to create the JWT filter class and a helper method for extracting the token from the request.

Watch the first segment where the JwtAuthenticationFilter class is created, extending OncePerRequestFilter (class creation). Then, pay close attention to the implementation of the helper method to extract the JWT from the "Authorization" header, which handles the "Bearer " prefix (token extraction).

Now, let's create this class in your project.

  1. Create a new package named com.example.groupmanager.config.
  2. Inside this package, create a new Java class named JwtAuthenticationFilter.
  3. This class needs access to our JwtService (to validate the token) and UserDetailsService (to load user data). We'll inject these using @Autowired.

Here is the complete code for JwtAuthenticationFilter.java. We'll break down the doFilterInternal method right after.

package com.example.groupmanager.config;

import com.example.groupmanager.service.JwtService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtService jwtService;
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(
            @NonNull HttpServletRequest request,
            @NonNull HttpServletResponse response,
            @NonNull FilterChain filterChain) throws ServletException, IOException {

        // 1. Get the Authorization header from the request
        final String authHeader = request.getHeader("Authorization");

        // 2. Check if the header is null or doesn't start with "Bearer "
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response); // Pass to the next filter
            return;
        }

        // 3. Extract the JWT from the header
        final String jwt = authHeader.substring(7); // "Bearer ".length() is 7

        // 4. Extract the username from the JWT
        final String username = jwtService.extractUsername(jwt);

        // 5. If username exists and user is not already authenticated
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            
            // 6. Load user details from the database
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

            // 7. If the token is valid for this user
            if (jwtService.isTokenValid(jwt, userDetails)) {
                
                // 8. Create an authentication token
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                        userDetails,
                        null, // Credentials are not needed as we use JWT
                        userDetails.getAuthorities()
                );
                
                // 9. Set details from the request
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                
                // 10. Update the SecurityContextHolder
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }
        
        // 11. Pass the request to the next filter in the chain
        filterChain.doFilter(request, response);
    }
}

Breaking Down doFilterInternal

The logic might seem complex, but it's a clear, sequential process. The article from OneUptime provides a well-commented code block that mirrors what we just wrote.

How to Implement JWT Authentication with Spring Security

This article provides an excellent breakdown of the JwtAuthenticationFilter. Reading through this section will reinforce your understanding of the code we just implemented.

Please read the section titled "JWT Authentication Filter". Focus on the sequence of checks: extracting the header, parsing the JWT, loading user details, and finally, updating the SecurityContextHolder.

The most critical part of this logic is step #10:
SecurityContextHolder.getContext().setAuthentication(authToken);

This single line is what tells Spring Security, "For this specific request, the user is authenticated." Once this is set, Spring's authorization mechanisms (like the @PreAuthorize annotations we'll cover later) can use the user's details and authorities to make access control decisions.


Step 2: Integrating the Filter into the Security Chain

We've created our filter, but Spring Security doesn't know about it yet. We need to explicitly add it to the security filter chain in our SecurityConfig class. The goal is to place our JwtAuthenticationFilter before the standard UsernamePasswordAuthenticationFilter. This ensures our token-based logic runs first for every request.

The following video from Telusko clearly explains the concept and the configuration change required.

#38 Spring Security | Validating JWT Token

This video from Telusko explains why and how to add a custom filter to the Spring Security chain.

Watch the segment from this timestamp. The explanation of placing a custom JWT filter before the default UsernamePasswordAuthenticationFilter is the key takeaway here.

Let's apply this to our SecurityConfig.java.

  1. First, inject the JwtAuthenticationFilter bean into SecurityConfig.
  2. Then, in the securityFilterChain method, use .addFilterBefore() to add it to the chain.

Update your SecurityConfig.java file:

// In SecurityConfig.java

// ... other imports
import com.example.groupmanager.config.JwtAuthenticationFilter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private CustomUserDetailsService userDetailsService;
    
    // Inject the custom filter
    @Autowired
    private JwtAuthenticationFilter jwtAuthFilter;

    // ... your PasswordEncoder and AuthenticationManager beans are here ...
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/auth/login").permitAll()
                .requestMatchers(HttpMethod.POST, "/users").hasRole("ADMIN")
                .requestMatchers(HttpMethod.GET, "/users").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            // Add our custom JWT filter before the standard authentication filter
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
            
        return http.build();
    }
}

With this change, every request will now pass through our JwtAuthenticationFilter before any other authentication processing.


Step 3: Testing the Complete Authentication Flow

This is where all the pieces come together. Let's verify that our filter is working correctly.

Restart your Spring Boot application.

  1. Test without a token:

    • Open Postman and create a GET request to a protected endpoint, for example, http://localhost:8080/users.
    • Do not add any authorization.
    • Send the request. You should receive a 403 Forbidden response. This confirms the endpoint is protected.
  2. Obtain a token:

    • Use the POST /auth/login request you created in the last lesson.
    • Send the admin user's credentials ({"username": "admin", "password": "password"}).
    • You will receive a JWT in the response body. Copy this token string.
  3. Test with a valid token:

    • Go back to your GET /users request in Postman.
    • Click on the "Authorization" tab.
    • Select "Bearer Token" from the "Type" dropdown.
    • Paste the copied JWT into the "Token" field.
    • Send the request.

This time, you should receive a 200 OK response with the list of users. Success! Your JwtAuthenticationFilter correctly validated the token and granted access.


Conclusion

Fantastic work! You have now implemented the most critical piece of a stateless JWT authentication system. You created a custom filter that validates tokens on every request, effectively making your API accessible to authenticated clients while keeping it secure from unauthorized access.

Key Takeaways:

  • A custom filter extending OncePerRequestFilter is the standard way to add request-processing logic to the Spring Security chain.
  • The filter is responsible for extracting the JWT from the Authorization: Bearer <token> header.
  • The SecurityContextHolder is used to store the authentication details for a successfully validated user for the scope of a single request.
  • The .addFilterBefore(myFilter, UsernamePasswordAuthenticationFilter.class) configuration is used to insert your custom filter at the correct position in the chain.

In this lesson and the previous one, we configured our session management policy to STATELESS. In our next lesson, we will delve deeper into what this means, ensuring our application relies completely on the JWT for its security state and does not create or use any server-side HTTP sessions.