logo

Achieve Ultimate Excellence

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 and extractExpiration 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.

avatar
Article By,
Create by
Browse Articles by Related Categories
Browse Articles by Related Tags
Share Article on:

Related posts