Skip to main content

Welcome to a Grasp lesson

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

Lesson 65

Implementing JWT Validation Filters

Recommended resources

Hello! In our last lesson, we built a login endpoint that authenticates users and issues a JSON Web Token (JWT). We now have a secure way of verifying a user's identity and giving them a "digital key." However, this key doesn't unlock any doors yet. Our API endpoints are still protected, but they don't know how to recognize or validate this JWT.

Today, we will complete the authentication loop by creating a JWT validation filter. This filter will act as a gatekeeper for our protected API endpoints. It will intercept every incoming request, look for a JWT in the Authorization header, and if a valid token is found, it will establish the user's identity for that request. This is the crucial step that transforms our token from a simple string into a functioning access pass.

By the end of this lesson, you will have a fully secured API where access to protected resources is granted only to requests carrying a valid JWT.

The Role of Filters in Spring Security

Before we write any code, let's understand the core concept. Spring Security protects your application by using a series of filters, known as the SecurityFilterChain. Think of it as an assembly line of security checks. When an HTTP request comes in, it passes through this chain, and each filter has the opportunity to inspect or modify the request before it reaches your controller.

Our goal is to add a custom filter to this chain. Its specific job will be to handle JWT authentication. We'll build our filter by extending OncePerRequestFilter, a convenient base class from Spring that guarantees our filter logic is executed exactly once for every request.


Implementing the JWT Authentication Filter

Let's create the filter. This single class will contain the entire logic for validating a token and authenticating the user for a given request.

Spring Boot Security - JWT Filter

The video "Spring Boot Security - JWT Filter" provides a great walkthrough for creating the filter. We will follow its structure but adapt it to our existing codebase, which uses constructor injection.

Watch the initial setup from this section to understand the creation of the JWTAuthenticationFilter class extending OncePerRequestFilter. Then, observe how the token is extracted from the request header in the clip from this part. We'll implement this logic next.

Create a new package named config or security, and inside it, create a new class JwtAuthenticationFilter.java.

package com.example.yourproject.config;

import com.example.yourproject.service.JwtService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.NonNull;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    public JwtAuthenticationFilter(JwtService jwtService, UserDetailsService userDetailsService) {
        this.jwtService = jwtService;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(
            @NonNull HttpServletRequest request,
            @NonNull HttpServletResponse response,
            @NonNull FilterChain filterChain
    ) throws ServletException, IOException {
        final String authHeader = request.getHeader("Authorization");

        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        final String jwt = authHeader.substring(7); // "Bearer ".length() is 7
        
        // TODO: Extract username from token
        // TODO: Validate token and update SecurityContext
    }
}

Let's break down this initial structure:

  1. We inject JwtService (which we'll complete shortly) and UserDetailsService (which we created in the last lesson).
  2. The core logic is in doFilterInternal.
  3. We first check the Authorization header. If it's missing or doesn't start with Bearer , we simply pass the request to the next filter in the chain and stop processing. This is crucial for allowing access to public endpoints like /auth/login.
  4. If a bearer token is present, we extract the JWT string itself by removing the "Bearer " prefix.

Completing the JwtService for Validation

In the last lesson, our JwtService had placeholder methods for validation. Now it's time to implement them. These methods will use the jjwt library to parse the token and extract its information, known as "claims".

Open your JwtService.java and add the following methods.

// In JwtService.java

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
// ... other imports

@Service
public class JwtService {

    // ... existing SECRET_KEY, JWT_EXPIRATION, and generateToken methods

    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    public boolean isTokenValid(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
    }

    private boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    private Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }

    private Claims extractAllClaims(String token) {
        return Jwts
                .parserBuilder()
                .setSigningKey(getSignInKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
    
    // getSignInKey() and getJwtExpiration() from previous lesson
}

Here's what these new methods do:

  • extractAllClaims: This is the private helper that does the heavy lifting. It parses the JWT using our secret key and returns all the claims contained within it.
  • extractClaim: A generic helper that allows us to extract a specific claim (like subject or expiration) from the claims body.
  • extractUsername: Uses extractClaim to get the "subject" claim, which holds the username.
  • isTokenValid: This is our main validation method. It performs two checks:
    1. Does the username in the token match the user details loaded from the database?
    2. Is the token expired?

Updating the Security Context

Now we can complete our JwtAuthenticationFilter. Once we've validated the token, we need to tell Spring Security who the current user is. We do this by updating the SecurityContextHolder.

Spring Boot Security - JWT Filter

The video demonstrates this exact process.

Watch how the presenter validates the token and extracts the username. Then, see how a UsernamePasswordAuthenticationToken is created and set in the security context from this crucial segment.

Let's add this logic to the doFilterInternal method in JwtAuthenticationFilter.java.

// In JwtAuthenticationFilter.java, inside doFilterInternal method

final String jwt = authHeader.substring(7);
final String username = jwtService.extractUsername(jwt);

// Check if user exists and is not already authenticated
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
    UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

    if (jwtService.isTokenValid(jwt, userDetails)) {
        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                userDetails,
                null, // Credentials are not needed as we use JWT
                userDetails.getAuthorities()
        );
        
        authToken.setDetails(
                new WebAuthenticationDetailsSource().buildDetails(request)
        );
        
        // Update SecurityContextHolder
        SecurityContextHolder.getContext().setAuthentication(authToken);
    }
}

// Always pass the request to the next filter
filterChain.doFilter(request, response);

Let's analyze the new code block:

  1. We extract the username from the token.
  2. The if condition checks that we have a username and that the user is not already authenticated. This prevents redundant checks.
  3. We use our UserDetailsService to load the User object from the database.
  4. If the token is valid for this user, we create a UsernamePasswordAuthenticationToken. This object is what Spring Security uses internally to represent the currently authenticated user.
  5. We set this authToken in the SecurityContextHolder. This is the key step. From this point forward, for the duration of this request, Spring Security knows the user is authenticated.
  6. Finally, we call filterChain.doFilter to continue the request processing.

Adding the Filter to the Security Chain

Our filter is built, but Spring Security doesn't know about it yet. We need to explicitly add it to the SecurityFilterChain.

Spring Boot Security - JWT Filter

This part of the video explains how to add the custom filter to the configuration.

Watch from this section to see how addFilterBefore is used to place our custom filter in the correct position in the chain.

Open your SecurityConfig.java file and modify the securityFilterChain bean.

// In SecurityConfig.java

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final AuthenticationProvider authenticationProvider;
    private final JwtAuthenticationFilter jwtAuthFilter; // Inject our filter

    public SecurityConfig(
        AuthenticationProvider authenticationProvider, 
        JwtAuthenticationFilter jwtAuthFilter // Add to constructor
    ) {
        this.authenticationProvider = authenticationProvider;
        this.jwtAuthFilter = jwtAuthFilter;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/**").permitAll() // Allow auth endpoints
                .anyRequest().authenticated() // Secure all other endpoints
            )
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authenticationProvider(authenticationProvider) // Set our custom provider
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); // Add our filter

        return http.build();
    }
}

The critical line is .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class). This tells Spring Security to run our jwtAuthFilter before its standard UsernamePasswordAuthenticationFilter. This is because we are handling authentication with a token, not a traditional username and password form submission for every request.


Testing the Full Authentication Flow

Everything is now in place. Let's test it. To do that, we need a protected endpoint.

  1. Create a Test Controller:

    // Create a new controller, e.g., GreetingsController.java
    
    package com.example.yourproject.controller;
    
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/api/greetings")
    public class GreetingsController {
    
        @GetMapping
        public ResponseEntity<String> sayHello() {
            return ResponseEntity.ok("Hello from a secured endpoint!");
        }
    }
    
  2. Run and Test:
    Start your application and use a tool like Postman.

    • Test 1: Accessing without a token.
      Send a GET request to http://localhost:8080/api/greetings. You should receive a 403 Forbidden response. This is because our filter chain ran, found no Authorization header, and the request proceeded to the authorization step, which requires authentication for this endpoint.

    • Test 2: Getting a token.
      Send a POST request to http://localhost:8080/auth/login with the correct credentials for a registered user. Copy the token from the response body.

    • Test 3: Accessing with a valid token.
      Create a new GET request to http://localhost:8080/api/greetings.

      • Go to the "Authorization" tab.
      • Select "Bearer Token" as the type.
      • Paste the token you copied into the "Token" field.
      • Send the request. You should now receive a 200 OK response with the message "Hello from a secured endpoint!".
    • Test 4: Accessing with an invalid token.
      Try sending the same request but modify the token slightly (e.g., delete a character). You should again receive a 403 Forbidden error, because the token signature will no longer be valid.


Conclusion

Excellent work! You have now implemented the complete, stateless JWT authentication flow. We've built a custom filter that acts as a bouncer for our API, checking the credentials (the JWT) of every request to a protected resource.

Key Takeaways:

  • Spring Security uses a FilterChain to apply security rules to incoming requests.
  • We can add custom logic by creating a filter, typically by extending OncePerRequestFilter.
  • The JWT validation filter is responsible for extracting the token, validating its signature and expiration, and loading user details.
  • The SecurityContextHolder is updated with an Authentication object to make the user "logged in" for the duration of the request.
  • The custom filter must be registered in the SecurityFilterChain using addFilterBefore to place it correctly.

Our API is now secure and functional. However, for other developers (or even ourselves in the future) to use it effectively, it needs clear documentation. In our next lesson, we will address this by integrating Swagger (OpenAPI) to automatically generate interactive API documentation.