Spring Boot

Common JWT Errors in Spring Boot: Identification and Practical Fixes

By Utility Zone · 2025-11-01T17:17:18.179623

JWT implementation in Spring Boot presents several recurring challenges that developers frequently encounter. This guide identifies the most prevalent issues and provides practical, tested solutions.

JWT Implementation Error Troubleshooting Flowchart

JWT Implementation Error Troubleshooting Flowchart


1. JWT Signature Mismatch Error

Problem: The application throws SignatureException: JWT signature does not match locally computed signature.123

Root Cause: The most common cause is using different secret keys for token generation and validation. This occurs when the secret key is dynamically generated (e.g., using new SecretKey() on each call) rather than using a consistent, stored value.2

Solution:

Store the secret key as a static constant in your JWT utility class:

@Component
public class JwtUtils {
    @Value("${jwt.secret}")
    private String secret;
    
    // Store the signing key once - NOT regenerating each time
    private SecretKey getSignKey() {
        byte[] keyBytes = Decoders.BASE64.decode(secret);
        return Keys.hmacShaKeyFor(keyBytes);
    }
    
    public String generateToken(UserDetails userDetails) {
        return Jwts.builder()
                .subject(userDetails.getUsername())
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(new Date(System.currentTimeMillis() + 3600000))
                .signWith(getSignKey(), SignatureAlgorithm.HS512)
                .compact();
    }
    
    public boolean validateToken(String token, UserDetails userDetails) {
        try {
            Jwts.parser()
                    .verifyWith(getSignKey())
                    .build()
                    .parseSignedClaims(token);
            return true;
        } catch (SignatureException e) {
            // Use same key for validation
            return false;
        }
    }
}

Key Point: Ensure that application.properties contains a properly encoded secret key:

jwt.secret=yourBase64EncodedSecretKeyHere

2. Invalid Token 401 Unauthorized Errors

Problem: Requests with valid JWT tokens still return 401 Unauthorized responses.456

Root Causes:

  • Authorization header is missing the "Bearer " prefix
  • Token is not being extracted from the header correctly
  • JWT authentication filter is not in the correct position in the filter chain
  • Token validation is failing silently

Solution:

Implement a proper JWT authentication filter with correct header parsing:

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Autowired
    private JwtUtils jwtUtils;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    private static final String BEARER_PREFIX = "Bearer ";
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
            HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        
        String authHeader = request.getHeader("Authorization");
        
        if (authHeader != null && authHeader.startsWith(BEARER_PREFIX)) {
            String jwtToken = authHeader.substring(BEARER_PREFIX.length());
            
            try {
                String username = jwtUtils.extractUsername(jwtToken);
                
                if (username != null && 
                    SecurityContextHolder.getContext().getAuthentication() == null) {
                    
                    UserDetails userDetails = 
                        userDetailsService.loadUserByUsername(username);
                    
                    if (jwtUtils.validateToken(jwtToken, userDetails)) {
                        UsernamePasswordAuthenticationToken authentication =
                                new UsernamePasswordAuthenticationToken(
                                    userDetails, null, 
                                    userDetails.getAuthorities());
                        authentication.setDetails(
                            new WebAuthenticationDetailsSource()
                                .buildDetails(request));
                        SecurityContextHolder.getContext()
                            .setAuthentication(authentication);
                    }
                }
            } catch (ExpiredJwtException e) {
                logger.warn("JWT token has expired");
            } catch (UnsupportedJwtException e) {
                logger.error("JWT token is unsupported");
            } catch (MalformedJwtException e) {
                logger.error("Invalid JWT token");
            } catch (IllegalArgumentException e) {
                logger.error("JWT claims string is empty");
            }
        }
        
        filterChain.doFilter(request, response);
    }
}

Configure the filter in the correct position within the filter chain:78

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) 
            throws Exception {
        http
            .csrf().disable()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/**").permitAll()
                .anyRequest().authenticated())
            // Add JWT filter BEFORE UsernamePasswordAuthenticationFilter
            .addFilterBefore(jwtAuthenticationFilter(), 
                UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
    
    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() {
        return new JwtAuthenticationFilter();
    }
}

3. CORS Issues with JWT Authorization Headers

Problem: CORS preflight requests fail when using JWT tokens in the Authorization header.9

Symptoms: Access-Control-Allow-Origin header is missing error on OPTIONS requests, but API works with Postman.

Root Cause: CORS configuration doesn't include Authorization in allowed headers, or CORS is not configured to run before Spring Security filters.

Solution:

Configure CORS properly in Spring Security to run before authentication:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList(
            "http://localhost:3000", 
            "http://localhost:8080"));
        configuration.setAllowedMethods(Arrays.asList(
            "GET", "POST", "PUT", "DELETE", "OPTIONS"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setExposedHeaders(Arrays.asList("Authorization"));
        configuration.setAllowCredentials(true);
        configuration.setMaxAge(3600L);
        
        UrlBasedCorsConfigurationSource source = 
            new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) 
            throws Exception {
        http
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .csrf().disable()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .addFilterBefore(jwtAuthenticationFilter(), 
                UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
}

Important: When using allowCredentials(true), avoid using wildcard (*) for origins and headers.1011


4. Session Creation Despite Stateless Configuration

Problem: JSESSIONID cookies appear in responses even though SessionCreationPolicy.STATELESS is configured.1213

Root Cause: The HttpSessionRequestCache still creates sessions for requests that fail authentication before reaching the JWT filter.

Solution:

Explicitly disable request cache to prevent session creation on authentication failure:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) 
            throws Exception {
        http
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .csrf().disable()
            .requestCache().disable()  // Disable request cache
            .and()
            .exceptionHandling()
                .authenticationEntryPoint((request, response, authException) -> {
                    response.sendError(HttpServletResponse.SC_UNAUTHORIZED, 
                        "Unauthorized");
                })
            .and()
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/**").permitAll()
                .anyRequest().authenticated())
            .addFilterBefore(jwtAuthenticationFilter(), 
                UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
}

Alternatively, disable all stateful features explicitly:14

http.csrf(AbstractHttpConfigurer::disable)
    .formLogin(AbstractHttpConfigurer::disable)
    .logout(AbstractHttpConfigurer::disable)
    .rememberMe(AbstractHttpConfigurer::disable)
    .httpBasic(AbstractHttpConfigurer::disable)
    .sessionManagement(session -> 
        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

5. Token Expiration and Clock Skew Issues

Problem: Tokens are rejected as expired immediately after creation, or with messages like "The ID Token contains invalid claims: {iat=...}".1516

Root Cause: Typically caused by clock skew—misalignment between the server that issued the token and the server validating it. Different server clocks can drift by seconds or minutes.17

Solution:

Configure clock skew tolerance in token validation:

@Component
public class JwtUtils {
    
    public Claims extractAllClaims(String token) {
        return Jwts.parser()
                .verifyWith(getSignKey())
                .clockSkewSeconds(60)  // Allow 60 seconds of skew
                .build()
                .parseSignedClaims(token)
                .getPayload();
    }
    
    public boolean isTokenValid(String token, UserDetails userDetails) {
        try {
            Claims claims = extractAllClaims(token);
            String username = claims.getSubject();
            Date expiration = claims.getExpiration();
            
            return username.equals(userDetails.getUsername()) 
                   && expiration.after(new Date());
        } catch (ExpiredJwtException e) {
            logger.warn("Token expired at: " + e.getClaims().getExpiration());
            return false;
        }
    }
}

Best Practice: Keep clockSkewSeconds between 5-30 seconds. Values larger than this indicate potential system configuration issues that should be investigated separately (NTP synchronization, system time settings).17


6. Audience Claim Mismatch

Problem: Error message: "The audience is invalid" or "audience does not match".181920

Root Cause: The JWT's aud (audience) claim doesn't match the API's expected audience value. The authorization server may not be issuing tokens with the correct audience, or the API is configured to validate an incorrect audience.18

Solution:

When generating tokens, include the correct audience:

public String generateToken(UserDetails userDetails) {
    return Jwts.builder()
            .subject(userDetails.getUsername())
            .issuedAt(new Date())
            .expiration(new Date(System.currentTimeMillis() + 3600000))
            .audience().add("myapi").and()  // Set audience explicitly
            .signWith(getSignKey(), SignatureAlgorithm.HS512)
            .compact();
}

And validate the audience during token verification:

public boolean validateToken(String token) {
    try {
        Claims claims = Jwts.parser()
                .verifyWith(getSignKey())
                .build()
                .parseSignedClaims(token)
                .getPayload();
        
        List<String> audience = claims.getAudience();
        return audience != null && audience.contains("myapi");
    } catch (Exception e) {
        return false;
    }
}

7. Malformed and Unsupported JWT Exceptions

Problem: Exceptions like MalformedJwtException, UnsupportedJwtException are thrown but not caught by custom exception handlers.21

Root Cause: Exceptions thrown within filters don't reach the controller advice or custom exception handlers automatically. They need to be handled within the filter itself or with a custom authentication entry point.22

Solution:

Implement a custom authentication entry point to handle JWT-specific exceptions:

@Component
public class JwtAuthenticationEntryPoint 
        implements AuthenticationEntryPoint {
    
    @Override
    public void commence(HttpServletRequest request,
            HttpServletResponse response,
            AuthenticationException authException)
            throws IOException {
        
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        
        Map<String, Object> errorDetails = new HashMap<>();
        errorDetails.put("message", authException.getMessage());
        errorDetails.put("timestamp", new Date());
        errorDetails.put("status", HttpServletResponse.SC_UNAUTHORIZED);
        
        // Determine specific error type
        if (authException.getCause() instanceof ExpiredJwtException) {
            errorDetails.put("error", "Token expired");
        } else if (authException.getCause() instanceof MalformedJwtException) {
            errorDetails.put("error", "Invalid token format");
        } else if (authException.getCause() instanceof UnsupportedJwtException) {
            errorDetails.put("error", "Token type not supported");
        } else {
            errorDetails.put("error", "Authentication failed");
        }
        
        ObjectMapper mapper = new ObjectMapper();
        response.getWriter()
            .write(mapper.writeValueAsString(errorDetails));
    }
}

Configure this in security config:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) 
        throws Exception {
    http
        .exceptionHandling()
            .authenticationEntryPoint(jwtAuthenticationEntryPoint)
        .and()
        .addFilterBefore(jwtAuthenticationFilter(), 
            UsernamePasswordAuthenticationFilter.class);
    
    return http.build();
}

8. Custom Claims Extraction and Validation

Problem: Custom claims added to JWT tokens are not properly extracted or validated in the application.23

Root Cause: Mismatch between claim names used during token generation and those configured in JwtAuthenticationConverter.

Solution:

Create a proper JWT converter that handles custom claims:

@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = 
        new JwtGrantedAuthoritiesConverter();
    grantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
    grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
    
    JwtAuthenticationConverter jwtAuthenticationConverter = 
        new JwtAuthenticationConverter();
    jwtAuthenticationConverter
        .setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
    
    // Extract custom claims
    jwtAuthenticationConverter.setPrincipalClaimName("sub");
    
    return jwtAuthenticationConverter;
}

When generating tokens, include custom claims:

public String generateToken(UserDetails userDetails, 
        Map<String, Object> additionalClaims) {
    Map<String, Object> claims = new HashMap<>(additionalClaims);
    claims.put("roles", userDetails.getAuthorities());
    
    return Jwts.builder()
            .claims(claims)
            .subject(userDetails.getUsername())
            .issuedAt(new Date())
            .expiration(new Date(System.currentTimeMillis() + 3600000))
            .signWith(getSignKey(), SignatureAlgorithm.HS512)
            .compact();
}

9. Refresh Token Implementation Issues

Problem: Access tokens expire but there's no mechanism to obtain new tokens without re-authenticating.242526

Solution:

Implement refresh token logic with separate expiration times:

@Component
public class JwtUtils {
    @Value("${jwt.access.expiration:3600000}")
    private int accessTokenExpiration;
    
    @Value("${jwt.refresh.expiration:86400000}")
    private int refreshTokenExpiration;
    
    public String generateAccessToken(UserDetails userDetails) {
        return createToken(userDetails, accessTokenExpiration);
    }
    
    public String generateRefreshToken(UserDetails userDetails) {
        return createToken(userDetails, refreshTokenExpiration);
    }
    
    private String createToken(UserDetails userDetails, int expirationMs) {
        return Jwts.builder()
                .subject(userDetails.getUsername())
                .issuedAt(new Date())
                .expiration(new Date(System.currentTimeMillis() + expirationMs))
                .signWith(getSignKey(), SignatureAlgorithm.HS512)
                .compact();
    }
    
    public boolean isTokenExpired(String token) {
        try {
            Claims claims = extractAllClaims(token);
            return claims.getExpiration().before(new Date());
        } catch (ExpiredJwtException e) {
            return true;
        }
    }
}

Create a refresh endpoint:

@RestController
@RequestMapping("/auth")
public class AuthController {
    
    @Autowired
    private JwtUtils jwtUtils;
    
    @PostMapping("/refresh")
    public ResponseEntity<?> refreshToken(
            @RequestBody RefreshTokenRequest request) {
        
        String refreshToken = request.getRefreshToken();
        
        if (jwtUtils.validateToken(refreshToken)) {
            String username = jwtUtils.extractUsername(refreshToken);
            UserDetails userDetails = 
                userDetailsService.loadUserByUsername(username);
            
            String newAccessToken = 
                jwtUtils.generateAccessToken(userDetails);
            
            return ResponseEntity.ok(
                new TokenResponse(newAccessToken, refreshToken));
        }
        
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
    }
}

10. CSRF Disabled Incorrectly

Problem: CSRF is disabled globally, potentially leaving the application vulnerable in certain scenarios.2728

Solution:

For stateless REST APIs using JWT, CSRF can be safely disabled:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) 
            throws Exception {
        http
            .csrf().disable()  // Safe for JWT-based stateless APIs
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/**").permitAll()
                .anyRequest().authenticated())
            .addFilterBefore(jwtAuthenticationFilter(), 
                UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
}

Important: Disabling CSRF is acceptable only when:

  • The application is stateless (using JWT)
  • Tokens are stored in headers (not cookies)
  • Session creation policy is STATELESS
  • No cookies are used for authentication

Best Practices Summary

When implementing JWT in Spring Boot, follow these guidelines:

Configuration: Use application properties to externalize JWT secrets and expiration times rather than hardcoding values.

Key Management: Store secret keys as single instances, not regenerated per request.

Filter Ordering: Place JWT authentication filters before UsernamePasswordAuthenticationFilter in the security chain.

Exception Handling: Implement custom authentication entry points to gracefully handle JWT-specific exceptions.

CORS Configuration: Configure CORS before Spring Security and include Authorization in allowed headers.

Clock Synchronization: Monitor server time synchronization and use reasonable clock skew values (5-30 seconds).

Token Validation: Always validate both signature and expiration, and verify custom claims match expected values.

Refresh Tokens: Implement refresh token mechanism with longer expiration than access tokens for better user experience. <span style="display:none">293031323334353637383940414243444546</span>


<div align="center">⁂</div>

Footnotes

  1. https://www.youtube.com/watch?v=aPzwQv89qXY ↩

  2. https://www.youtube.com/watch?v=DZCO9own9mg ↩ ↩2

  3. https://stackoverflow.com/questions/65306718/io-jsonwebtoken-signatureexception-jwt-signature-does-not-match-locally-compute ↩

  4. https://stackoverflow.com/questions/72595460/jwt-authentication-returns-401-unauthorized-spring-boot ↩

  5. https://stackoverflow.com/questions/52028457/spring-security-with-jwt-always-returns-401-unauthorized ↩

  6. https://www.youtube.com/watch?v=ucx6wo6dp98 ↩

  7. https://springframework.guru/using-filters-in-spring-web-applications/ ↩

  8. https://openillumi.com/en/en-spring-security-filter-chain-order/ ↩

  9. https://www.reddit.com/r/SpringBoot/comments/1ccnrav/strange_cors_problem_when_using_authorization/ ↩

  10. https://docs.spring.io/spring-security/reference/servlet/integrations/cors.html ↩

  11. https://reflectoring.io/spring-cors/ ↩

  12. https://stackoverflow.com/questions/52573539/spring-adds-a-jsessionid-despite-stateless-session-management ↩

  13. https://github.com/spring-projects/spring-security/issues/11733 ↩

  14. https://skryvets.com/blog/2024/12/15/spring-auth-jwt/ ↩

  15. https://community.auth0.com/t/spring-boot-sample-app-invalid-id-token/60794 ↩

  16. https://learn.microsoft.com/en-us/answers/questions/880950/(invalid-id-token)-an-error-occurred-while-attempt ↩

  17. https://stackoverflow.com/questions/47153080/clock-skew-and-tokens ↩ ↩2

  18. https://openillumi.com/en/en-fix-jwt-audience-invalid-identityserver/ ↩ ↩2

  19. https://community.auth0.com/t/issue-with-audience-does-not-match-error-in-token-validation/113524 ↩

  20. https://stackoverflow.com/questions/65230299/invalid-audience-claim-in-token-the-json-web-token-jwt-used-as-a-token-does-no ↩

  21. https://stackoverflow.com/questions/75627422/unable-to-catch-malformedjwtexception-in-spring-boot-with-custom-exception-handl ↩

  22. https://reflectoring.io/spring-security-jwt/ ↩

  23. https://sultanov.dev/blog/custom-jwt-claims-in-spring-security-oauth/ ↩

  24. https://www.javainuse.com/webseries/spring-security-jwt/chap7 ↩

  25. https://www.bezkoder.com/spring-boot-refresh-token-jwt/ ↩

  26. https://github.com/bezkoder/spring-security-refresh-token-jwt ↩

  27. https://www.reddit.com/r/SpringBoot/comments/124nnk5/csrf_disable/ ↩

  28. https://www.linkedin.com/pulse/how-configure-cors-csrf-spring-boot-3-security-6-payman-asemany-gohar-qbudf ↩

  29. https://devforum.okta.com/t/jwt-validation-in-spring-boot-failing/22593 ↩

  30. https://www.youtube.com/watch?v=ZhtF4i-iB1A ↩

  31. https://learn.microsoft.com/en-us/answers/questions/833682/authorization-failed-to-authenticate-since-the-jwt ↩

  32. https://stackoverflow.com/questions/54886687/expire-the-jwt-token-on-logout-in-spring-boot-rest-api ↩

  33. https://github.com/bezkoder/spring-boot-refresh-token-jwt ↩

  34. https://www.reddit.com/r/SpringBoot/comments/uovkt8/jwt_token_does_not_start_with_bearer/ ↩

  35. https://github.com/cuongld2/springboot-simpleAPI/issues/4 ↩

  36. https://www.youtube.com/watch?v=KxqlJblhzfI ↩

  37. https://github.com/spring-projects/spring-boot/issues/31142 ↩

  38. https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html ↩

  39. https://github.com/adrianhajdin/threads/issues/86 ↩

  40. https://github.com/orgs/community/discussions/135618 ↩

  41. https://www.reddit.com/r/csharp/comments/1ikv7dv/what_is_purpose_of_jwts_clockskew/ ↩

  42. https://stackoverflow.com/questions/44824382/how-to-disable-csrf-in-spring-using-application-properties ↩

  43. https://ssojet.com/jwt-validation/validate-jwt-using-rs256-in-spring-boot/ ↩

  44. https://stackoverflow.com/questions/78837595/how-to-extract-jwt-claims-in-springboot ↩

  45. https://www.danvega.dev/blog/spring-security-cors ↩

  46. https://stackoverflow.com/questions/79123990/csrf-and-jwt-with-sping-security-6-stateless-rest ↩