Stateless Authentication with JWT and Spring Security: A Developer's Handbook
JSON Web Tokens (JWT) have become a popular method for securing web applications. In the context of Spring Security, JWT provides a robust and scalable solution for authentication and authorization. This blog post will guide you through the process of integrating JWT with Spring Security, covering everything from the basics of JWT to a step-by-step implementation.
Introduction to JWT
JSON Web Tokens (JWT) are an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. A JWT typically consists of three parts:
-
Header: Contains metadata about the token, such as the algorithm used for signing.
-
Payload: Contains the claims or the actual data.
-
Signature: Ensures the integrity of the token.
Why Use JWT with Spring Security?
Integrating JWT with Spring Security offers several advantages:
-
Stateless Authentication: JWT enables stateless authentication, meaning the server does not need to store session information.
-
Scalability: Easily scales across multiple servers.
-
Flexibility: Can be used across different domains and platforms.
Setting Up the Project
Integrating JWT with Spring Security requires setting up a Spring Boot project with the necessary dependencies and configurations. Follow these steps to get started:
Create a New Spring Boot Project
You can create a new Spring Boot project using the Spring Initializr or your preferred IDE.
-
Spring Initializr: Visit Spring Initializr and select the required dependencies (e.g., Spring Web, Spring Security).
-
IDE: Most modern IDEs like IntelliJ IDEA or Eclipse have options to create a new Spring Boot project.
Add Dependencies
Add the necessary dependencies for Spring Security and JWT to your pom.xml
file.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
Configure Application Properties
You may need to configure specific application properties, such as the JWT secret key or expiration time. Add these to your application.properties
or application.yml
file.
jwt.secret-key=mysecretkey
jwt.expiration-time=3600000
Implementation with Spring Security & JWT
Integrating JWT with Spring Security involves several key steps. Below, we'll walk through the process of creating JWT utility classes, configuring security, and setting up filters. Key components include:
-
JwtUtil Class: Responsible for generating, validating, and extracting information from JWT tokens.
-
SecurityConfig Class: Configures Spring Security to work with JWT, including authentication and authorization rules.
-
AuthenticationManager: Authenticates users and is exposed as a bean to be used in the authentication endpoint.
-
JwtRequestFilter: A custom filter that intercepts requests to validate JWT tokens, ensuring that all requests are authenticated.
-
JwtResponseFilter: An optional filter to generate and send JWT tokens in the response after successful authentication, although this functionality is typically handled within the authentication endpoint.
Creating the JWT Util Class
The JWT Util class is responsible for generating, validating, and parsing JWT tokens. Below is the implementation of the JwtUtil
class, utilizing the defined property jwt.secret-key=mysecretkey
. This class includes methods for generating, validating, and extracting information from JWT tokens.
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Service
public class JwtUtil {
@Value("${jwt.secret-key}")
private String SECRET_KEY;
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
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() + 1000 * 60 * 60 * 10)) // 10 hours
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
}
This class provides the following functionalities:
-
Generating Tokens: The
generateToken
method creates a JWT token for a given user. -
Validating Tokens: The
validateToken
method checks if the token is valid for a given user and has not expired. -
Extracting Information: The
extractUsername
andextractExpiration
methods allow you to retrieve the username and expiration date from the token.
The @Value
annotation is used to inject the secret key from the properties file, ensuring that the same secret key (mysecretkey
) is used throughout the application.
Make sure to include the appropriate JWT library in your project dependencies to utilize these functionalities.
Configuring Security - SecurityConfig
Below is the implementation of the SecurityConfig
class using WebSecurityConfigurerAdapter
. This class configures Spring Security to work with JWT authentication.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService myUserDetailsService;
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/authenticate").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
Here's a breakdown of the key components:
-
UserDetailsService: Custom user details service to load user-related data.
-
JwtRequestFilter: Custom filter to validate JWT tokens in incoming requests.
-
AuthenticationManagerBuilder: Configures authentication to use the custom user details service and password encoder.
-
HttpSecurity: Configures security settings, such as disabling CSRF, setting up URL-based authorization, and enabling stateless sessions.
-
PasswordEncoder: Defines a password encoder bean to handle password encoding (using BCrypt in this case).
-
AuthenticationManager: Exposes the authentication manager bean to be used elsewhere in the application.
This configuration ensures that all requests (except the authentication endpoint) are secured and require a valid JWT token. The jwtRequestFilter
is applied to every request to validate the token before processing the request.
Make sure to define the JwtRequestFilter
and UserDetailsService
beans in your application to match this configuration.
Creating JWT Request and Response Filters
JWT Request Filter
The JwtRequestFilter
is a custom filter that intercepts HTTP requests to validate JWT tokens. Below is the full implementation of this filter:
import io.jsonwebtoken.ExpiredJwtException;
import org.springframework.beans.factory.annotation.Autowired;
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.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;
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@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);
try {
username = jwtUtil.extractUsername(jwt);
} catch (ExpiredJwtException e) {
// Handle token expiration if needed
}
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}
Here's a breakdown of what this code does:
-
Extracts the JWT Token: The filter looks for the "Authorization" header in the request and extracts the JWT token.
-
Extracts the Username: It uses the
JwtUtil
class to extract the username from the token. -
Loads User Details: It uses the
UserDetailsService
to load the user details based on the extracted username. -
Validates the Token: It validates the token using the
JwtUtil
class. -
Sets Authentication: If the token is valid, it sets the authentication in the security context, so the user is authenticated for the duration of the request.
-
Continues the Filter Chain: Finally, it continues the filter chain by calling
chain.doFilter(request, response)
.
This filter ensures that every request (except those you've explicitly allowed in your security configuration) contains a valid JWT token, effectively securing your application's endpoints. Make sure to include this filter in your security configuration, as shown in the previous examples.
JWT Response Filter
The JwtResponseFilter
is typically used to generate and send JWT tokens in the response after successful authentication. However, in most JWT implementations, the token generation is handled directly within the authentication endpoint (e.g., /authenticate
) rather than in a separate filter.
That said, if you still want to create a separate filter for generating and sending JWT tokens, you can do so. Below is an example of how you might implement a JwtResponseFilter
:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;
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 JwtResponseFilter extends GenericFilterBean {
@Autowired
private JwtUtil jwtUtil;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String jwt = jwtUtil.generateToken(userDetails);
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setHeader("Authorization", "Bearer " + jwt);
}
chain.doFilter(request, response);
}
}
This filter checks if the user is authenticated and then uses the JwtUtil
class to generate a JWT token for the authenticated user. It then adds the token to the "Authorization" header in the response.
Please note that this approach might not be suitable for all use cases. Typically, the JWT token is generated and sent in the response by the authentication endpoint itself, as it allows for more control over the response format and handling of authentication success and failure.
If you choose to use this filter, you'll need to carefully configure it within your security configuration to ensure that it's applied at the appropriate stage in the filter chain, and only after successful authentication.
Authentication Manager
The AuthenticationManager
is a crucial part of Spring Security that handles the authentication logic. It's an interface with a method authenticate
that takes an Authentication
object and returns a fully populated Authentication
object if the authentication is successful.
In a typical JWT setup, you don't need to provide a custom implementation of AuthenticationManager
since Spring Security provides a default implementation. However, you do need to expose it as a bean so that you can use it in other parts of your application.
Here's how you can do that, along with an example of how to use it in a REST controller for authentication.
1. Exposing AuthenticationManager as a Bean
In your SecurityConfig
class, you can define a bean for the AuthenticationManager
like this:
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
2. Using AuthenticationManager in a REST Controller
You can then use the AuthenticationManager
in a REST controller to authenticate users. Here's an example of a controller that uses the AuthenticationManager
to authenticate users and generate JWT tokens:
import org.springframework.beans.factory.annotation.Autowired;
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.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AuthenticationController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsService myUserDetailsService;
@PostMapping("/authenticate")
public ResponseEntity<?> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword())
);
} catch (BadCredentialsException e) {
throw new Exception("Incorrect username or password", e);
}
final UserDetails userDetails = myUserDetailsService.loadUserByUsername(authenticationRequest.getUsername());
final String jwt = jwtUtil.generateToken(userDetails);
return ResponseEntity.ok(new AuthenticationResponse(jwt));
}
}
In this example, the /authenticate
endpoint takes a username and password, and uses the AuthenticationManager
to authenticate the user. If the authentication is successful, it uses the JwtUtil
class to generate a JWT token, which is then returned to the client.
The AuthenticationRequest
and AuthenticationResponse
classes would be simple POJOs to hold the request and response data.
This setup integrates the AuthenticationManager
with the rest of the JWT authentication flow, providing a complete solution for authenticating users with JWT tokens in a Spring Security application.
Testing the Implementation
You can test the implementation using tools like Postman or write integration tests using Spring.
Best Practices
-
Use HTTPS: Always transmit JWT over HTTPS to ensure security.
-
Handle Expiration: Implement token expiration to mitigate risks.
-
Avoid Storing Sensitive Information: Never store sensitive information in the payload.
Conclusion
Integrating JWT with Spring Security provides a powerful and flexible solution for securing web applications. By following this guide, developers can implement a stateless, scalable, and robust authentication system.
By following the detailed steps and code snippets provided, developers can implement a robust, stateless, and secure authentication system using JWT with Spring Security. This approach offers scalability and flexibility, making it suitable for modern web applications.