Spring Security 6 with JWT Authentication

In this post I will show how to use Spring Security 6 with JWT Authentication to Secure Your App. I will do it using the Web Filters to handle both JWT requests and credentials requests.

Content

  • JWT Authentication
  • Web Filters
  • Dependencies
  • UsernamePasswordAuthFilter
  • JwtAuthFilter
  • UserAuthProvider
  • SecurityConfig
  • Authentication Principal Annotation

Look at the explanatory video for more details.

All the code of the article is available in the following repository.

JWT Authentication

The authentication is a critical point in the application. And its implementation must be as easy as possible. The more complexity I add the more errors it can contains. And I don’t want it. With Sprint Security 6 I can easily configure the protected routes and the way a user needs to authenticate. And of course, the JWT is the easiest way to secure a stateless application.

As JWT is a Json web token it allows me to store some user information, such as the user ID, name, validity date and other properties. Never critical information such as passwords or credit card numbers.

The backend creates the JWT and sends it to the front end. The front end will then use it for each request in the authorization header. The presence of the JWT in the request will authenticate the request.

A JWT has three parts: the header, to store the algorithm used to create the token; the payload contains the user data; and the signature. To generate the signature it uses a secret key and the first two parts of the token.

JWT structure

Web filters

As I don’t want to check in all the controllers and services the validity of the JWT, I will need something else. I will use the web filters. The web filters are placed before and after each controller. They are called sequentially before entering a controller. And when exiting a controller, they are called again.

Web Filters sequence

By default I have filters for the security context, the anonymous authentication, session management, exception handling and more. And I can add my custom filters in between.

I will add two filters. The first one will be to check the presence and validity of the JWT for each request. And the second one will be the regular authentication. The authentication with the username and password. Because the end user needs to authenticate in a way before trusting him and creating him a JWT.

Once I authenticate a request via the password or the JWT, what can I do? I will fill the security context. The security context contains the information about the authenticated user in an object. This object allows me to save: the user object when the authentication was already done; the credentials if I still need to authenticate the request; and the roles if I already know all about the authenticated user.

Dependencies

I first need to include the Sprint Security dependency in my pom XML. By default Spring Boot 3 uses a Sprint Security 6. And Sprint Security 6 requires Java 17. I will also include a library to create and validate the JWT.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.sergio.socialnetwork</groupId>
    <artifactId>backend-social-network</artifactId>
    <version>2.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.1</version>
    </parent>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>4.2.1</version>
        </dependency>

    </dependencies>
</project>

UsernamePasswordAuthFilter

Let’s now start with the first filter, the username and password filter.

public class UsernamePasswordAuthFilter extends OncePerRequestFilter {

    private static final ObjectMapper MAPPER = new ObjectMapper();

    private final UserAuthenticationProvider userAuthenticationProvider;

    public UsernamePasswordAuthFilter(UserAuthenticationProvider userAuthenticationProvider) {
        this.userAuthenticationProvider = userAuthenticationProvider;
    }

    @Override
    protected void doFilterInternal(
            HttpServletRequest httpServletRequest,
            HttpServletResponse httpServletResponse,
            FilterChain filterChain) throws ServletException, IOException {

        if ("/v1/signIn".equals(httpServletRequest.getServletPath())
                && HttpMethod.POST.matches(httpServletRequest.getMethod())) {
            CredentialsDto credentialsDto = MAPPER.readValue(httpServletRequest.getInputStream(), CredentialsDto.class);

            try {
                SecurityContextHolder.getContext().setAuthentication(
                        userAuthenticationProvider.validateCredentials(credentialsDto));
            } catch (RuntimeException e) {
                SecurityContextHolder.clearContext();
                throw e;
            }
        }

        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

I extend the OncePerRequest filter because I only need this filter to be run once per request.

I only want this filter to check the signIn URL.

The credentials from the request are read from the InputStream and mapped into an object.

I Validate the credentials with the provider calling the method validateCredentials. And if it’s correct fill the security context. Otherwise clear the security context and throw the exception.

And at the end, always call the doFilter to continue with the other filters.

I will create the provider in below.

JwtAuthFilter

Let’s continue now with the JWT filter.

public class JwtAuthFilter extends OncePerRequestFilter {

    private final UserAuthenticationProvider userAuthenticationProvider;

    public JwtAuthFilter(UserAuthenticationProvider userAuthenticationProvider) {
        this.userAuthenticationProvider = userAuthenticationProvider;
    }

    @Override
    protected void doFilterInternal(
            HttpServletRequest httpServletRequest,
            HttpServletResponse httpServletResponse,
            FilterChain filterChain) throws ServletException, IOException {
        String header = httpServletRequest.getHeader(HttpHeaders.AUTHORIZATION);

        if (header != null) {
            String[] authElements = header.split(" ");

            if (authElements.length == 2
                    && "Bearer".equals(authElements[0])) {
                try {
                    SecurityContextHolder.getContext().setAuthentication(
                            userAuthenticationProvider.validateToken(authElements[1]));
                } catch (RuntimeException e) {
                    SecurityContextHolder.clearContext();
                    throw e;
                }
            }
        }

        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

I extend the same filter as before.

I read the JWT which is present in the Authorization Header.

If the token is present and it’s a Bearer token, I try to validate it. Otherwise clear the security context and throw an exception.

And always at the end of each filter, call the doFilter.

UserAuthProvider

Let’s see now what the user provider looks like.

@Component
public class UserAuthenticationProvider {

    @Value("${security.jwt.token.secret-key:secret-key}")
    private String secretKey;

    private final AuthenticationService authenticationService;

    public UserAuthenticationProvider(AuthenticationService authenticationService) {
        this.authenticationService = authenticationService;
    }

    @PostConstruct
    protected void init() {
        // this is to avoid having the raw secret key available in the JVM
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    }

    public String createToken(String login) {
        Date now = new Date();
        Date validity = new Date(now.getTime() + 3600000); // 1 hour

        Algorithm algorithm = Algorithm.HMAC256(secretKey);
        return JWT.create()
                .withIssuer(login)
                .withIssuedAt(now)
                .withExpiresAt(validity)
                .sign(algorithm);
    }

    public Authentication validateToken(String token) {
        Algorithm algorithm = Algorithm.HMAC256(secretKey);

        JWTVerifier verifier = JWT.require(algorithm)
                .build();

        DecodedJWT decoded = verifier.verify(token);

        UserDto user = authenticationService.findByLogin(decoded.getIssuer());

        return new UsernamePasswordAuthenticationToken(user, null, Collections.emptyList());
    }

    public Authentication validateCredentials(CredentialsDto credentialsDto) {
        UserDto user = authenticationService.authenticate(credentialsDto);
        return new UsernamePasswordAuthenticationToken(user, null, Collections.emptyList());
    }


}

I must annotate it with the @Component annotation to be accessible to inject it anywhere.

To generate and validate the JWT, I need a secret key. I will put this secret key in the configuration file and inject it here.

To avoid having the secret key readable in the JVM, I will encode it in base64.

When creating a token in the method createToken, I put a validity of one hour. After that that, the user will need to sign in again. Inside the token I store the login information, the creation date, and the validity date.

For the method validateToken, I receive an existing token. The method JWTVerifier.verify at line 37 will throw an exception if the JWT is expired. If the token is valid, I just go look for the user and create the authentication user bean.

Finally, I also need to validate a user request which comes with the credentials in the method validateCredentials.

SecurityConfig

Let’s continue creating the Sprint Security configuration. I will add those filters and protect my endpoints.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final UserAuthenticationEntryPoint userAuthenticationEntryPoint;
    private final UserAuthenticationProvider userAuthenticationProvider;

    public SecurityConfig(UserAuthenticationEntryPoint userAuthenticationEntryPoint,
                          UserAuthenticationProvider userAuthenticationProvider) {
        this.userAuthenticationEntryPoint = userAuthenticationEntryPoint;
        this.userAuthenticationProvider = userAuthenticationProvider;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .exceptionHandling().authenticationEntryPoint(userAuthenticationEntryPoint)
                .and()
                .addFilterBefore(new UsernamePasswordAuthFilter(userAuthenticationProvider), BasicAuthenticationFilter.class)
                .addFilterBefore(new JwtAuthFilter(userAuthenticationProvider), UsernamePasswordAuthFilter.class)
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeHttpRequests((requests) -> requests
                        .requestMatchers(HttpMethod.POST, "/v1/signIn", "/v1/signUp").permitAll()
                        .anyRequest().authenticated())
                ;
        return http.build();
    }
}

I first need to add the annotation @EnableWebSecurity.

In the securityFilterChain method, I start by adding the entry point to manage the exceptions.

Then, in lines 19 and 20, I add the two filters I’ve created. I want the JWT filter first, then the username password filter. All these before any other authentication filter of spring.

I disabled the CSRF at line 21.

I say that I’m in a stateless application in the line 22. I won’t need any session management.

And at line 25 permit only the signIn and signUp requests without authentication. The rest of the requests need an authenticated user.

Finally build the security configuration and return it.

Authentication Principal Annotation

Let’s now see how can I use the authenticated user.

    @GetMapping("/messages")
    public ResponseEntity<List<MessageDto>> getCommunityMessages(
            @AuthenticationPrincipal UserDto user,
            @RequestParam(value = "page", defaultValue = "0") int page) {
        return ResponseEntity.ok(communityService.getCommunityMessages(user, page));
    }

Now, in the controllers, I can use the @AuthenticationPrincipal annotation. This annotation injects the user bean from the security context. This means that if the request reaches the controller, this object will have the value of the authenticated user.

    @PostMapping("/signIn")
    public ResponseEntity<UserDto> signIn(@AuthenticationPrincipal UserDto user) {
        user.setToken(userAuthenticationProvider.createToken(user.getLogin()));
        return ResponseEntity.ok(user);
    }

I need the signIn endpoint to allow the user to authenticate via a username and a password. After the authentication is successful, I return a JWT. The JWT will be then used for the following requests in the HTTP header.

Conclusion

  • I’ve added the dependency of Spring Security and ensure I’m using Java 17.
  • I’ve added the JWT filter to read and validate the JWT in the HTTP header for each request.
  • I’ve added the username password filter to read and validate the username and the password for the signIn endpoint.
  • I’ve then created the Spring Security configuration by adding the filters, the protected URLs, exception handling and session management.
  • Finally, the signIn endpoint. I must return the created JWT if the user signed it in correctly with its username and password.

References

All the code of the article is available in the following repository.

My New ebook, How to Master Git With 20 Commands, is available now.

One response to “Spring Security 6 with JWT Authentication”

  1. Еxcellent post. I will be dealing with somе of these іssues as well..

    Like

Leave a comment

A WordPress.com Website.