Skip to main content

Spring Security JWT

Introduction to JWT with Spring Security

JSON Web Token (JWT) has become the industry standard for securing RESTful APIs in modern applications. When combined with Spring Security, JWT provides a stateless, scalable solution for authentication and authorization.

In this tutorial, we'll learn how to implement JWT-based authentication in Spring Security, allowing us to create secure APIs that can be consumed by various clients like web applications, mobile apps, or other services.

What is JWT?

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed.

A JWT token consists of three parts:

  1. Header - Contains metadata about the token, such as the algorithm used for signing
  2. Payload - Contains claims or assertions about the user and additional data
  3. Signature - Verifies the token hasn't been altered

These parts are Base64-encoded and separated by dots, resulting in a structure like:

xxxxx.yyyyy.zzzzz

Why Use JWT with Spring Security?

  • Stateless Authentication: No need to store session information on the server
  • Scalability: Works well in distributed systems and microservices
  • Cross-domain/CORS: Can be easily used across domains
  • Decoupled/Mobile-ready: Perfect for APIs consumed by various clients

Setting Up JWT in Spring Security

Let's walk through implementing JWT authentication step by step.

Step 1: Add Dependencies

First, add these dependencies to your pom.xml file:

xml
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- JWT Library -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>

Step 2: Create JWT Utility Class

Let's create a utility class to handle JWT operations like token generation and validation:

java
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

@Component
public class JwtUtil {

private final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
private final long jwtExpirationMs = 86400000; // 24 hours

// Generate token for user
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}

// Create token with claims and subject
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
.signWith(key)
.compact();
}

// Validate token
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}

// Extract username from token
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}

// Extract expiration date from token
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}

// Check if token is expired
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}

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

// Extract all claims from token
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
}

Step 3: Create a JwtAuthenticationFilter

Next, we need a filter to intercept requests and validate JWT tokens:

java
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtUtil jwtUtil;
private final UserDetailsService userDetailsService;

public JwtAuthenticationFilter(JwtUtil jwtUtil, UserDetailsService userDetailsService) {
this.jwtUtil = jwtUtil;
this.userDetailsService = userDetailsService;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {

final String authorizationHeader = request.getHeader("Authorization");

String username = null;
String jwt = null;

if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
username = jwtUtil.extractUsername(jwt);
}

if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());

authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request));

SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(request, response);
}
}

Step 4: Configure Security

Now, configure Spring Security to use our JWT filter:

java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

private final JwtAuthenticationFilter jwtAuthenticationFilter;

public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/authenticate").permitAll() // Auth endpoint is public
.anyRequest().authenticated() // All other endpoints require auth
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // No session
.and()
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}

Step 5: Authentication Controller

Create an endpoint to authenticate users and generate tokens:

java
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class AuthenticationController {

private final AuthenticationManager authenticationManager;
private final UserDetailsService userDetailsService;
private final JwtUtil jwtUtil;

public AuthenticationController(
AuthenticationManager authenticationManager,
UserDetailsService userDetailsService,
JwtUtil jwtUtil) {
this.authenticationManager = authenticationManager;
this.userDetailsService = userDetailsService;
this.jwtUtil = jwtUtil;
}

@PostMapping("/authenticate")
public ResponseEntity<?> createAuthenticationToken(@RequestBody AuthenticationRequest request) throws Exception {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
);
} catch (BadCredentialsException e) {
throw new Exception("Incorrect username or password", e);
}

final UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
final String token = jwtUtil.generateToken(userDetails);

return ResponseEntity.ok(new AuthenticationResponse(token));
}
}

// Request and response models
class AuthenticationRequest {
private String username;
private String password;

// Getters and setters
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}

class AuthenticationResponse {
private final String token;

public AuthenticationResponse(String token) {
this.token = token;
}

public String getToken() { return token; }
}

Step 6: Protected Resource

Let's create a simple protected endpoint to test our JWT authentication:

java
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class ResourceController {

@GetMapping("/hello")
public String hello() {
return "Hello, this is a protected endpoint!";
}
}

Using JWT Authentication

Now that we have set up JWT authentication, here's how to use it:

1. Obtain a JWT Token

First, authenticate to get a token:

bash
curl -X POST 'http://localhost:8080/api/authenticate' \
-H 'Content-Type: application/json' \
-d '{
"username": "user",
"password": "password"
}'

Response:

json
{
"token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiZXhwIjoxNjI1MDY2MjQ5LCJpYXQiOjE2MjQ5Nzk4NDl9.lGNVs8SID5cCU4Kghe8PKeRm5Ih-9XtjmCGpt4G9GaQ"
}

2. Access Protected Resources

Use the token in the Authorization header:

bash
curl -X GET 'http://localhost:8080/api/hello' \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiZXhwIjoxNjI1MDY2MjQ5LCJpYXQiOjE2MjQ5Nzk4NDl9.lGNVs8SID5cCU4Kghe8PKeRm5Ih-9XtjmCGpt4G9GaQ'

Response:

Hello, this is a protected endpoint!

Real-World JWT Implementation Best Practices

When implementing JWT in production applications, consider these best practices:

1. Token Expiration

Set appropriate expiration times for tokens. Short-lived tokens (e.g., 15 minutes) reduce the risk if a token is compromised.

java
private final long jwtExpirationMs = 900000; // 15 minutes

2. Use Refresh Tokens

Implement a refresh token strategy to improve user experience while maintaining security:

java
public String generateRefreshToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000)) // 7 days
.signWith(key)
.compact();
}

3. Secure Your Secret Key

In production, never hardcode secret keys. Use environment variables or a secure key vault:

java
@Value("${jwt.secret}")
private String secretKey;

@PostConstruct
protected void init() {
key = Keys.hmacShaKeyFor(secretKey.getBytes());
}

4. Add Claims to JWT

Add additional claims to customize your tokens:

java
public String generateToken(UserDetails userDetails, Map<String, Object> additionalClaims) {
Map<String, Object> claims = new HashMap<>(additionalClaims);
return createToken(claims, userDetails.getUsername());
}

5. Error Handling for JWT

Handle token validation errors gracefully:

java
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{\"error\": \"Unauthorized\", \"message\": \""
+ authException.getMessage() + "\"}");
}
}

Then configure it in your security config:

java
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
// other configurations...
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
// continue with other configurations
.build();
}

Summary

In this tutorial, we've learned:

  1. How to implement JWT authentication in a Spring Security application
  2. The structure and purpose of JSON Web Tokens
  3. How to create a filter to validate JWT tokens
  4. How to configure Spring Security for stateless authentication
  5. Best practices for JWT implementation in real-world applications

JWT with Spring Security provides a robust solution for securing your APIs, especially in distributed systems and microservices architectures. By following the implementation steps and best practices outlined in this guide, you can create secure, scalable applications that protect your resources while providing a good user experience.

Additional Resources

Practice Exercises

  1. Implement refresh token functionality in the authentication controller
  2. Add role-based authorization using JWT claims
  3. Create a logout mechanism that invalidates tokens
  4. Enhance the JWT token with custom claims like user roles and permissions
  5. Implement token blacklisting for revoked tokens


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)