Protect your Spring Boot application with JWT

In this article, I will explain how to protect a Spring Boot application with a JWT using Spring Security.

In previous article, I’ve already created a Spring Boot project which is able to communicate with an Angular frontend configuring the CORS.

Then, I’ve shown how to connect a Spring Boot application to a PostgreSQL.

And finally, I’ve also created some endpoints to have the main CRUD operations, Create, Read, Update and Delete.

And now, it’s time to add Spring Security to protect the endpoints.

Content:

  • Spring Security and JWT Dependency
  • Password Encoder
  • Login Endpoint
  • JWT HTTP Filter
  • Spring Security Configuration
  • Authentication Principal Annotation

You can watch this video for a more detailed explanation.

All the code used in the article is available in this repository.

Spring Security and JWT Dependencies

If I want to protect my Spring Application with a JWT, I must first add two dependencies: Spring Security to protect the application, and JWT to manage the tokens.

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>4.3.0</version>
        </dependency>

The version number of the Spring Security dependency is not necessary, as I have as parent the Spring Boot Starter Parent project.

Password Encoder

When managing passwords in an application, I must ensure that the passwords are never stored in plain text. This is a very bad security practice. As if the database is compromised, the passwords will be accessible to any malicious user.

A Password Encoder will encode all the password with a one-way hashing algorithm . This means that I can obtain the hash from the password, but I can’t obtain the original password from the hash.

This way, storing the hash in the database won’t compromise the original passwords of the users.

And when I need to check the validity of a password, the Password Encoder will hash the given password and compare it with a hashed password.

To configure my Spring Boot application to use a Password Encoder, I only need to declare a bean with the desired Password Encoder implementation.

@Component
public class PasswordConfig {

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

I will show in the next chapter how to use the Password Encoder to check the validity of a given password.

Login Endpoint

The Login endpoint is the endpoint which reads the username and password from a user’s request and checks its validity. And it’s the only one not protected.

This is also the endpoint where the JWT is generated. This way, the frontend receives a JWT to be used for each following requests.

    @PostMapping("/login")
    public ResponseEntity<UserDto> login(@RequestBody @Valid CredentialsDto credentialsDto) {
        UserDto userDto = userService.login(credentialsDto);
        userDto.setToken(userAuthenticationProvider.createToken(userDto));
        return ResponseEntity.ok(userDto);
    }

The DTOs used in the previous code are very simple.

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UserDto {

    private Long id;
    private String firstName;
    private String lastName;
    private String login;
    private String token;

}
public record CredentialsDto (String login, char[] password) {

Why a char array in the CredentialsDto?

I come back to the chapter about the Password Encoder. This way, I don’t have a variable with the original password in plain text.

Having a String, the original password will be stored in the volatil memory of the JVM. If an attacker has access to this memory, it will has access to the plain password.

Having a char array, each letter will be saved in a different memory space. The attacker will need to read all the different letters, put them in order, and maybe split the different passwords of the different users. This is an impossible task.

Let’s see the UserService to check the validity of the credentials.

@RequiredArgsConstructor
@Service
public class UserService {

    private final UserRepository userRepository;

    private final PasswordEncoder passwordEncoder;

    private final UserMapper userMapper;

    public UserDto login(CredentialsDto credentialsDto) {
        User user = userRepository.findByLogin(credentialsDto.login())
                .orElseThrow(() -> new AppException("Unknown user", HttpStatus.NOT_FOUND));

        if (passwordEncoder.matches(CharBuffer.wrap(credentialsDto.password()), user.getPassword())) {
            return userMapper.toUserDto(user);
        }
        throw new AppException("Invalid password", HttpStatus.BAD_REQUEST);
    }
}

In the UserService, I can see how the PasswordEncoder is used to check the validity of the password.

JWT HTTP Filter

The Http Filters are classes that wrap all the request. They will wrap a request before going to a controller and after the controller finishes its execution.

I will create an Http Filter to read the JWT in the requests. If the JWT is valid, I let the request continue until the controller. If the JWT is invalid, I stop the execution and throw an exception.

The Http Filters are added to the application with the Spring Security configuration.

@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

    private final UserAuthenticationProvider userAuthenticationProvider;

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {
        String header = request.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(request, response);
    }
}

All what is before the line 28 will be executed before entering in the controller. And all what is after the line 28 will be executed after exiting the controller.

In this case, I only add logic before entering the controller. I only add the logic to validate the JWT before entering the controller. If the JWT is valid, the request is valid and the controller can execute its code.

I’ve used in the JWT filter a User Provider class. This class validates the JWT. It’s also used in the controller to generate a new token.

@Component
public class UserAuthenticationProvider {

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

    @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(UserDto user) {
        Date now = new Date();
        Date validity = new Date(now.getTime() + 3600000); // 1 hour

        Algorithm algorithm = Algorithm.HMAC256(secretKey);
        return JWT.create()
                .withSubject(user.getLogin())
                .withIssuedAt(now)
                .withExpiresAt(validity)
                .withClaim("firstName", user.getFirstName())
                .withClaim("lastName", user.getLastName())
                .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 = UserDto.builder()
                .login(decoded.getSubject())
                .firstName(decoded.getClaim("firstName").asString())
                .lastName(decoded.getClaim("lastName").asString())
                .build();

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

As said, it contains only two methods: to validate a JWT, and to generate a JWT.

In the first case, it returns an Authentication object to be injected in the Spring Security Context Holder.

In the second case, it returns as a String the token.

Spring Security Configuration

Now I need to put all together, the protected routes and the JWT filter. This will all go into the Spring Security configuration.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .addFilterBefore(new JwtAuthFilter(userAuthenticationProvider), BasicAuthenticationFilter.class)
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(customizer -> customizer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests((requests) -> requests
                        .requestMatchers(HttpMethod.POST, "/login", "/register").permitAll()
                        .anyRequest().authenticated())
        ;
        return http.build();
    }
}

In the configuration, I first include the JWT filter with the needed dependency of the user provider. I include the JWT filter before the Basic Authentication. This way, even if the Basic Authentication is enable by error, the JWT will always be the primary authentication system.

For simplicity, I disable the CSRF. You can find more information about the CSRF in the following link.

Finally, I indicate that the login endpoint is the only one where all the requests are permitted, while the rest of the endpoint only accept authenticated requests.

Authentication Principal Annotation

Once I have the Authentication object in the Security Context Holder, I can use the annotation Authentication Principal in the controllers.

    @GetMapping("/messages")
    public ResponseEntity<List<String>> messages(@AuthenticationPrincipal UserDto dto) {
        return ResponseEntity.ok(Arrays.asList("first", dto.firstName(), "second", dto.lastName()));
    }

The AuthenticationPrincipal annotation allows me to have access to the authenticated user.

Conclusion

To have a fully protected backend, I need two things: configure the protected routes and have an HTTP Filter which validates the requests.

Those are the most important points to have a fully protected backend. If I ensure that all the routes are correctly protected, and the HTTP Filter verifies correctly each request (and throws an exception if it’s not the case), the rest of the application doesn’t need to worry about the security.

All the code used in the article is available in this repository.

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

One response to “Protect your Spring Boot application with JWT”

  1. […] Of course, this Angular application depends on a backend to generate and validate the JWT. The backend part is done with Spring Boot and available in the following article. […]

    Like

Leave a comment

A WordPress.com Website.