CodeOverflow

234
votes

Best practices for JWT authentication in Spring Boot

I'm implementing JWT authentication in my Spring Boot application and want to make sure I'm following security best practices. Here's my current implementation:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
            .antMatchers("/api/auth/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        
        http.addFilterBefore(jwtAuthenticationFilter(), 
                            UsernamePasswordAuthenticationFilter.class);
    }
}

My questions are:

  1. Should I store the JWT secret in application.properties or use environment variables?
  2. What's the recommended token expiration time?
  3. Should I implement refresh tokens?
  4. How do I properly handle token revocation?

5 Answers

456
votes

Excellent questions! Here's a comprehensive approach to JWT security in Spring Boot:

1. Secret Key Management

Never store secrets in application.properties. Use environment variables or a secrets management service like HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault:

@Value("${JWT_SECRET}")
private String jwtSecret;

// Better yet, use a proper key:
private Key getSigningKey() {
    byte[] keyBytes = Decoders.BASE64.decode(jwtSecret);
    return Keys.hmacShaKeyFor(keyBytes);
}

2. Token Expiration

Use short-lived access tokens (15-30 minutes) with refresh tokens:

public String generateAccessToken(UserDetails userDetails) {
    return Jwts.builder()
        .setSubject(userDetails.getUsername())
        .setIssuedAt(new Date())
        .setExpiration(new Date(System.currentTimeMillis() + 900000)) // 15 min
        .signWith(getSigningKey(), SignatureAlgorithm.HS512)
        .compact();
}

public String generateRefreshToken(UserDetails userDetails) {
    return Jwts.builder()
        .setSubject(userDetails.getUsername())
        .setIssuedAt(new Date())
        .setExpiration(new Date(System.currentTimeMillis() + 604800000)) // 7 days
        .signWith(getSigningKey(), SignatureAlgorithm.HS512)
        .compact();
}

3. Refresh Token Implementation

Absolutely! Store refresh tokens in a secure database with user association:

@Entity
public class RefreshToken {
    @Id
    private String token;
    
    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;
    
    private Instant expiryDate;
    
    // Add token family for rotation detection
    private String tokenFamily;
}

4. Token Revocation Strategy

Implement a token blacklist with Redis for fast lookups:

@Service
public class TokenBlacklistService {
    
    @Autowired
    private RedisTemplate redisTemplate;
    
    public void blacklistToken(String token, long expirationTime) {
        String jti = extractJti(token);
        long ttl = expirationTime - System.currentTimeMillis();
        
        redisTemplate.opsForValue().set(
            "blacklist:" + jti, 
            "revoked", 
            ttl, 
            TimeUnit.MILLISECONDS
        );
    }
    
    public boolean isTokenBlacklisted(String token) {
        String jti = extractJti(token);
        return redisTemplate.hasKey("blacklist:" + jti);
    }
}

Additional Security Recommendations:

  • Always use HTTPS in production
  • Implement rate limiting on auth endpoints
  • Add a JTI (JWT ID) claim for unique token identification
  • Consider implementing token binding to prevent token theft
  • Use refresh token rotation for enhanced security
  • Implement proper CORS configuration

This approach balances security with usability. The short-lived access tokens minimize exposure if compromised, while refresh tokens allow seamless user experience.

89
votes

Great answer above! I'd also add that you should consider using Spring Security's built-in OAuth2 Resource Server support for JWT validation. It's more robust and well-tested:

@Configuration
@EnableWebSecurity
public class ResourceServerConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(authorize -> authorize
                .antMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .decoder(jwtDecoder())
                )
            );
    }
    
    @Bean
    public JwtDecoder jwtDecoder() {
        SecretKey key = Keys.hmacShaKeyFor(jwtSecret.getBytes());
        return NimbusJwtDecoder.withSecretKey(key).build();
    }
}

This gives you automatic token validation, claim extraction, and better error handling out of the box!