Tuesday, January 7, 2025

The Crux of JWT-Based Authentication using Spring Security

In modern web applications, JSON Web Tokens (JWT) are widely used for securing APIs due to their stateless and scalable nature. In this post, we'll explore the core idea behind implementing JWT-based authentication in a Spring Security setup, including configuring public URLs for Actuator and Swagger, and handling unauthorized access with a custom authentication entry point.

Core Idea: How JWT Authentication Works

JWT authentication in Spring Security involves a few key steps:

  1. Extract and Validate the JWT:
    • Parse the incoming request's JWT token (usually from the Authorization header).
    • Verify the token's signature, expiration, and claims.
  2. Retrieve User Details:
    • Extract the username and roles from the token's claims.
  3. Set Security Context:
    • Create an Authentication object with the user's details and granted authorities.
    • Set the Authentication object in the SecurityContextHolder.
  4. Authorize Requests:
    • Spring Security uses the information in the SecurityContextHolder to authorize requests based on roles and permissions.

Key Components

JWT Filter

A custom filter is needed to intercept requests, validate JWTs, and populate the security context.

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtService jwtService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String token = extractToken(request);

        if (token != null && jwtService.validateToken(token)) {
            String username = jwtService.extractUsername(token);
            List<GrantedAuthority> authorities = jwtService.extractRoles(token).stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());

            Authentication authentication = new UsernamePasswordAuthenticationToken(username, null, authorities);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }

    private String extractToken(HttpServletRequest request) {
        String header = request.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            return header.substring(7);
        }
        return null;
    }
}

Security Configuration

The filter must be added before the UsernamePasswordAuthenticationFilter. Additionally, we’ll configure public URLs for Actuator and Swagger, and a custom entry point for handling unauthorized access.

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtFilter) throws Exception {
        http.csrf().disable()
            .authorizeHttpRequests()
                .antMatchers("/swagger-ui/**", "/v3/api-docs/**", "/actuator/**").permitAll()
                .anyRequest().authenticated()
            .and()
            .exceptionHandling()
                .authenticationEntryPoint(customAuthenticationEntryPoint())
            .and()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public AuthenticationEntryPoint customAuthenticationEntryPoint() {
        return (request, response, authException) -> {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json");
            response.getWriter().write("{\\"error\\":\\"Unauthorized\\"}");
        };
    }
}

JWT Service

A utility class for handling JWT-related operations, such as validation and claims extraction.

@Service
public class JwtService {

    private final String secretKey = "your-secret-key";

    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
            return true;
        } catch (JwtException e) {
            return false;
        }
    }

    public String extractUsername(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
    }

    public List<String> extractRoles(String token) {
        Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody();
        return claims.get("roles", List.class);
    }
}

Testing the Setup

  1. JWT Validation:
    • Send a request with a valid JWT in the Authorization header: Authorization: Bearer <your-token>.
    • The application should authenticate the user and allow access based on roles.
  2. Public Endpoints:
    • Access http://localhost:8080/actuator/health or http://localhost:8080/swagger-ui.html without a JWT to verify public access.
  3. Unauthorized Access:
    • Send a request to a secured endpoint without a JWT.
    • You should receive a 401 response with the custom error message: {"error":"Unauthorized"}.

Conclusion

JWT-based authentication in Spring Security provides a clean and scalable way to secure APIs. By setting up a custom filter, configuring public endpoints, and handling unauthorized access gracefully, you can create a robust security mechanism tailored to your application's needs. Let me know your thoughts or if you'd like more details on extending this setup!

No comments:

Post a Comment