Spring Boot JWT and OAuth2 in production: Full Config
The most surprising thing about JWT and OAuth2 in production is that they’re often implemented as a flimsy security blanket, rather than a robust, multi-layered defense system.
Let’s see it in action. Imagine a user logs in.
// User service to authenticate and generate JWT
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private JwtTokenProvider jwtTokenProvider; // Our JWT utility
public String authenticateUser(String username, String password) {
User user = userRepository.findByUsername(username);
if (user != null && passwordEncoder.matches(password, user.getPassword())) {
return jwtTokenProvider.generateToken(user.getUsername());
}
throw new BadCredentialsException("Invalid username or password");
}
}
// JwtTokenProvider utility
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration}")
private long jwtExpiration; // in milliseconds
public String generateToken(String username) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpiration);
return Jwts.builder()
.setSubject(username)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
return true;
} catch (JwtException e) {
// Log the exception properly in a real app
return false;
}
}
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody();
return claims.getSubject();
}
}
This user service authenticates credentials and, if valid, uses JwtTokenProvider to create a signed JWT. This token is then sent back to the client. The client includes this JWT in the Authorization header of subsequent requests, typically as Bearer <token>.
Our security configuration intercepts these requests:
// SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter(jwtTokenProvider);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors().and().csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // JWT is stateless
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll() // Allow auth endpoints
.anyRequest().authenticated(); // All other requests require auth
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
// ... other beans like PasswordEncoder, etc.
}
// JwtAuthenticationFilter.java
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private JwtTokenProvider jwtTokenProvider;
public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && jwtTokenProvider.validateToken(jwt)) {
String username = jwtTokenProvider.getUsernameFromToken(jwt);
// Create an Authentication object and set it in the SecurityContext
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
username, null, null); // Authorities can be added here if needed
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7); // "Bearer ".length() == 7
}
return null;
}
}
This SecurityConfig tells Spring Security to disable sessions (STATELESS) because JWT handles state. It permits access to authentication endpoints and requires authentication for everything else. The JwtAuthenticationFilter intercepts every request, extracts the JWT, validates it, and if valid, populates Spring Security’s SecurityContext with the user’s identity.
The application.properties would look something like this:
jwt.secret=a_very_strong_and_long_secret_key_that_should_be_rotated_regularly
jwt.expiration=604800000 # 7 days in milliseconds
The problem this solves is stateless authentication. Without JWT, you’d typically rely on server-side sessions. When a user logs in, the server creates a session, stores a session ID in a cookie, and the client sends that cookie back. This requires the server to maintain session state for every active user, which becomes a scaling bottleneck. JWT, on the other hand, embeds the user’s identity and claims directly into the token. The server only needs to verify the token’s signature, not look up session data.
OAuth2 adds another layer, typically for third-party authorization or delegating authentication. A common pattern is using an OAuth2 Authorization Server (like Keycloak, Auth0, or even Spring Authorization Server) to issue JWTs. Your Spring Boot application then acts as a Resource Server, validating the JWTs issued by the Authorization Server.
The mental model is:
- Authentication: User provides credentials. A trusted entity (your app or an external auth server) verifies them.
- Token Issuance: A signed token (JWT) is generated, containing user identity and potentially permissions/roles. This token is short-lived.
- Token Presentation: The client sends the token with every request.
- Token Validation: The server validates the token’s signature and expiry. If valid, it trusts the identity within.
- Authorization: Based on the validated identity (and claims in the token), the server decides if the user can perform the requested action.
One of the most overlooked aspects of JWT security is the aud (audience) claim. While iss (issuer) and exp (expiration) are standard, the aud claim specifies who the token is intended for. In a microservices architecture, a JWT issued by an authentication service might be intended for service-a. If service-b receives this token and doesn’t check the aud claim, it might mistakenly authorize a user who was only meant to access service-a, leading to privilege escalation. Properly validating the aud claim ensures that a token issued for one service isn’t accepted by another.
The next concept you’ll encounter is managing token revocation and refresh tokens for a better user experience and security.