You are on page 1of 38

Spring Boot

Implement JWT authentication


Spring Boot 3 application in a
Eric Cabrel TIOGO
Oct 5, 2023 • 16 min read

Photo by NEOM / Unsplash


Sell products, services,
content and more with
Squarespace.
ADS VIA CARBON

In the ever-evolving landscape of web development and application


security, the need for robust authentication mechanisms has never
been more critical. When exposing an API to the public, ensuring only
authorized access to the resources is essential.
One of the most popular and effective authentication methods in
modern web applications is JSON Web Tokens (JWT). It provides a
flexible and stateless way to verify the identity of users and secure API
endpoints; it is also called Token-Based Authentication.
In this article, we will explore the implementation of JWT authentication
in a Spring Boot 3 application.

What we will build


The API must expose routes where some are accessible without
authentication while others require one. The table below enumerates
them:
API ROUTE ACCESS STATUS DESCRIPTION

[POST] /auth/signup Unprotected Register a new user

[POST] /auth/login Unprotected Authenticate a user

[GET] /users/me Protected Retrieve the current authenticated user

[GET] /users Protected Retrieve all the users


Prerequisites
You must need these tools installed on your computer to follow this
tutorial.
JDK 11 or higher - Download link
Maven 3.8 or higher - Download link
MySQL running on Docker - Download's link
An HTTP client such as Postman, Insomnia, cURL, etc.
We need Docker to run a container for MySQL 8; you can skip it if
MySQL is installed on your computer. Run the command below to start
the Docker container from the MySQL image:

docker run -d -e MYSQL_ROOT_PASSWORD=secret -e MYSQL_DATABASE=taskdb --name

Set up the project


The Spring Boot project needs these four dependencies:
The Spring Web: to build Web, including RESTful applications
using Spring MVC. It uses Apache Tomcat as the default
embedded container.
The Spring Security: Allows implementing authentication and
access-based control.
The Spring Data JPA: Persist data in SQL stores with Java
Persistence API using Spring Data and Hibernate.
MySQL Driver for Java: The MySQL driver makes it possible to
interact with a database from a Java application.
The Spring Boot online project starter helps us create the project with
these dependencies; go to the URL start.spring.io to generate a new
project.

Generate a Spring Boot project with the dependencies.

I chose Java 17 and Maven as the dependency manager, but you can
use whatever you want.
Click on the button "Generate" to download the project, open it in your
IDE, and install the Maven dependencies.
Configure the database connection
Let's configure the application to connect to the database and perform
database table creation (using Hibernate under the hood). Open the
application configuration file src/resources/application.properties
and add the code below:

server.port=8005

spring.datasource.url=jdbc:mysql://localhost:3307/taskdb?serverTimezone=UTC&all
spring.datasource.username=root
spring.datasource.password=secret

## Hibernate properties
spring.jpa.hibernate.ddl-auto=update
spring.jpa.open-in-view=false

Run the application with the command mvn spring-boot:run ; it will


start at port 8005.
The Start-up of a Spring Boot application.

If we pay attention to the console when starting the application, we can


see the message displaying a security password generated because
Spring Security has the HTTP Basic authentication enabled by default.

Install JSON Web Token dependencies


We need a library to encode, decode, and validate the JWT token in the
application. We will use JJWT, so open the pom.xml and the code below
in the "dependencies" XML tag:

<dependencies>
<!---- existing dependencies here....... ---->
<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>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
</dependencies>

Save and run mvn install to install the dependencies.

Create the user entity


We perform authentication to ensure the users accessing the system
are known, meaning they are stored in the database.
The first step is to define the user in our system, and it is done by
creating a JPA Entity that will generate the related table in the
database. It is recommended to keep the history of database changes
using migrations; I wrote a post about using Flyway to handle database
migrations.
Inside the package entities create a file User.java and add the
code below:

package com.tericcabrel.authapi.entities;

import jakarta.persistence.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.util.Date;

@Table(name = "users")
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(nullable = false)
private Integer id;
@Column(nullable = false)
private String fullName;
@Column(unique = true, length = 100, nullable = false)
private String email;
@Column(nullable = false)
private String password;
@CreationTimestamp
@Column(updatable = false, name = "created_at")
private Date createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private Date updatedAt;
// Getters and setters
}

Inside the package , create a file


repositories
UserRepository.java ; it represents the Data access layer for the User
entity. Add the code below:

package com.tericcabrel.authapi.repositories;

import com.tericcabrel.authapi.entities.User;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface UserRepository extends CrudRepository<User, Integer> {
Optional<User> findByEmail(String email);
}

The function findByEmail() will be used later when implementing the


user authentication.
Run the application with the command mvn spring-boot:run ; The
table "users" will be created in the database.

View the user table created with Hibernate.

Extend the User Entity with authentication details


To manage user details related to authentication, Spring Security
provides an Interface named "UserDetails" with properties and
methods that the User entity must override the implementation.
Update the file "User.java to" implement the UserDetails interface;
below is the code of the file:

@Table(name = "users")
@Entity
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(nullable = false)
private Integer id;
@Column(nullable = false)
private String fullName;
@Column(unique = true, length = 100, nullable = false)
private String email;
@Column(nullable = false)
private String password;
@CreationTimestamp
@Column(updatable = false, name = "created_at")
private Date createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private Date updatedAt;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of();
}

public String getPassword() {


return password;
}

@Override
public String getUsername() {
return email;
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}

// Getters and setters


}

The method "getAuthorities()" returns the user's roles list; it is helpful


to manage permissions. We return an empty list because we will not
cover role-based access control.
The method "getUsername()" returns the email address because it is
unique information about the user.
💡 Make sure the method isAccountNonExpired(), isAccountNonLocked(),
isCredentialsNonExpired(), and isEnabled() returns "true"; otherwise, the
authentication will fail. You can customize the logic of these methods to fit
your needs.

Create the JWT service


To generate, decode, or validate a JSON Web token, we must expose
the related methods that use the libraries we installed earlier. We will
group them into a service class named JwtService.
Create a package services , then add the file JwtService.java and
past the code below:

package com.tericcabrel.authapi.services;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

@Service
public class JwtService {
@Value("${security.jwt.secret-key}")
private String secretKey;
@Value("${security.jwt.expiration-time}")
private long jwtExpiration;
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}

public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {


final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}

public String generateToken(UserDetails userDetails) {


return generateToken(new HashMap<>(), userDetails);
}

public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {


return buildToken(extraClaims, userDetails, jwtExpiration);
}

public long getExpirationTime() {


return jwtExpiration;
}

private String buildToken(


Map<String, Object> extraClaims,
UserDetails userDetails,
long expiration
){
return Jwts
.builder()
.setClaims(extraClaims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expira
.signWith(getSignInKey(), SignatureAlgorithm.HS256)
.compact();
}

public boolean isTokenValid(String token, UserDetails userDetails) {


final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())) && !isTokenExpire
}

private boolean isTokenExpired(String token) {


return extractExpiration(token).before(new Date());
}

private Date extractExpiration(String token) {


return extractClaim(token, Claims::getExpiration);
}

private Claims extractAllClaims(String token) {


return Jwts
.parserBuilder()
.setSigningKey(getSignInKey())
.build()
.parseClaimsJws(token)
.getBody();
}

private Key getSignInKey() {


byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
}

The methods that will be used are generateToken() ,


isTokenValid() and getExpirationTime() .

To generate the JWT token, we need a secret key and the token
expiration time; these values are read from the application configuration
properties file using the annotation @Value.
We must update the application.properties to define these values:

security.jwt.secret-key=3cfa76ef14937c1c0ea519f8fc057a80fcd04a7420f8e8bcd0a7
# 1h in millisecond
security.jwt.expiration-time=3600000

The secret key must be an HMAC hash string of 256 bits; otherwise,
the token generation will throw an error. I used this website to generate
one.
The token expiration time is expressed in milliseconds, so remember if
your token expires too soon.

Override the security configuration


By default, the HTTP basic authentication, but we want to override it to
perform the:
Perform the authentication by finding the user in our database.
Generate a JWT token when the authentication succeeds.
To override the implementation, let's create a package configs , add
the file ApplicationConfiguration.java and add the code below:

package com.tericcabrel.authapi.configs;

import com.tericcabrel.authapi.repositories.UserRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProv
import org.springframework.security.config.annotation.authentication.configu
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundExcepti
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class ApplicationConfiguration {
private final UserRepository userRepository;
public ApplicationConfiguration(UserRepository userRepository) {
this.userRepository = userRepository;
}

@Bean
UserDetailsService userDetailsService() {
return username -> userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User not fou
}

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

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

@Bean
AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvi

authProvider.setUserDetailsService(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());

return authProvider;
}
}

The userDetailsService() defines how to retrieve the user using the


UserRepository that is injected.
The passwordEncoder() creates an instance of the
BCryptPasswordEncoder() used to encode the plain user password.
The authenticationProvider() sets the new strategy to perform the
authentication.
If you re-run your application at this step, you will not see the password
generated in the console as before. We have successfully overridden
the authentication method.

Create the authentication middleware


For every request, we want to retrieve the JWT token in the header
"Authorization", and validate it:
If the token is invalid, reject the request if the token is invalid or
continues otherwise.
If the token is valid, extract the username, find the related user
in the database, and set it in the authentication context so you
can access it in any application layer.
In the package configs , create a file
JwtAuthenticationFilter.java and add the code below that
implements everything explained previously:

package com.tericcabrel.authapi.configs;

import com.tericcabrel.authapi.services.JwtService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthentic
import org.springframework.security.core.Authentication;
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.WebAuthenticationDeta
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.servlet.HandlerExceptionResolver;

import java.io.IOException;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final HandlerExceptionResolver handlerExceptionResolver;
private final JwtService jwtService;
private final UserDetailsService userDetailsService;

public JwtAuthenticationFilter(
JwtService jwtService,
UserDetailsService userDetailsService,
HandlerExceptionResolver handlerExceptionResolver
){
this.jwtService = jwtService;
this.userDetailsService = userDetailsService;
this.handlerExceptionResolver = handlerExceptionResolver;
}

@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
if (authHeader == null
|| !authHeader.startsWith( "Bearer ")) {
filterChain.doFilter(request, response);
return;
}

try {
final String jwt = authHeader.substring( ); 7
final String userEmail = jwtService.extractUsername(jwt);

Authentication authentication = SecurityContextHolder.getContext

if (userEmail != null && authentication == null) {


UserDetails userDetails = this.userDetailsService.loadUserByU
if (jwtService.isTokenValid(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken = new Use
userDetails,
null,
userDetails.getAuthorities()
);

authToken.setDetails( new
WebAuthenticationDetailsSource
SecurityContextHolder.getContext().setAuthentication(aut
}
}

filterChain.doFilter(request, response);
} catch (Exception exception) {
handlerExceptionResolver.resolveException(request, response, nul
}
}
}

💡 A try-catch block wraps the logic and uses the HandlerExceptionResolver to


forward the error to the global exception handler. We will see how it can be
helpful to do the exception forwarding.

A great thing to do here is to use caching to find the user by his email
address to improve the performance. I wrote a blog post to show you
how to implement caching in the SpringBoot application.
Data Caching in a Spring Boot application with Redis
In this post, we will see how to use Redis to cache data
retrieved from a MySQL database through a Spring Boot…
Teco Tutorials • Eric Cabrel TIOGO

Configure the application requester filter


The custom authentication is ready, and the remaining thing is to define
what criteria an incoming request must match before being forwarded
to application middleware. We want the following criteria:
There is no need to provide the CSRF token because we will use
it.
The request URL path matching /auth/signup and
/auth/login doesn't require authentication.

Any other request URL path must be authenticated.


The request is stateless, meaning every request must be
treated as a new one, even if it comes from the same client or
has been received earlier.
Must use the custom authentication provider, and they must be
executed before the authentication middleware.
The CORS configuration must allow only POST and GET
requests.
In the package configs , create a file
JwtAuthenticationFilter.java and add the code below:

package com.tericcabrel.authapi.configs;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecur
import org.springframework.security.config.annotation.web.configuration.Enab
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthe
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
private final AuthenticationProvider authenticationProvider;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
public SecurityConfiguration(
JwtAuthenticationFilter jwtAuthenticationFilter,
AuthenticationProvider authenticationProvider
){
this.authenticationProvider = authenticationProvider;
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf()
.disable()
.authorizeHttpRequests()
.requestMatchers( "/auth/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordA

return http.build();
}

@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();

configuration.setAllowedOrigins(List.of("http://localhost:8005"));
configuration.setAllowedMethods(List.of("GET","POST"));
configuration.setAllowedHeaders(List.of("Authorization","Content-Type"))

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurat

source.registerCorsConfiguration( "/**",configuration);
return source;
}
}

Our authentication is done and ready to be tested.

Create the authentication service


This service will contain the logic for registering a new user and
authenticating an existing user.
Create a new package dtos that will have the DTOs for both actions.
Create the file RegisterUserDto.java and add the code below:

package com.tericcabrel.authapi.dtos;

public class RegisterUserDto {


private String email;
private String password;
private String fullName;

// getters and setters here...


}

Create the file LoginUserDto.java and add the code below:

package com.tericcabrel.authapi.dtos;

public class LoginUserDto {


private String email;
private String password;

// getters and setters here...


}

We didn't apply any validation in the DTO for brevity, and if you are
interested in learning how I wrote this complete blog post below to help
you:

Validate Request Body and Parameter in Spring Boot


In this tutorial, we will use Hibernate Validator to validate the
request body and URL parameters sent to a Spring Boot…
Teco Tutorials • Eric Cabrel TIOGO

In the package services , create a file AuthenticationService.java


and add the code below:
package com.tericcabrel.authapi.services;

import com.tericcabrel.authapi.dtos.LoginUserDto;
import com.tericcabrel.authapi.dtos.RegisterUserDto;
import com.tericcabrel.authapi.entities.User;
import com.tericcabrel.authapi.repositories.UserRepository;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthentic
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class AuthenticationService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;

private final AuthenticationManager authenticationManager;

public AuthenticationService(
UserRepository userRepository,
AuthenticationManager authenticationManager,
PasswordEncoder passwordEncoder
){
this.authenticationManager = authenticationManager;
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}

public User signup(RegisterUserDto input) {


User user = new User()
.setFullName(input.getFullName())
.setEmail(input.getEmail())
.setPassword(passwordEncoder.encode(input.getPassword()));

return userRepository.save(user);
}

public User authenticate(LoginUserDto input) {


authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
input.getEmail(),
input.getPassword()
)
);

return userRepository.findByEmail(input.getEmail())
.orElseThrow();
}
}

Create user registration and authentication


routes
We can now create the routes /auth/signup and /auth/login for
user registration and authentication, respectively.
Create a package controllers , add the file
AuthenticationController.java and add the code below:

import com.tericcabrel.authapi.entities.User;
import com.tericcabrel.authapi.dtos.LoginUserDto;
import com.tericcabrel.authapi.dtos.RegisterUserDto;
import com.tericcabrel.authapi.responses.LoginResponse;
import com.tericcabrel.authapi.services.AuthenticationService;
import com.tericcabrel.authapi.services.JwtService;
import org.springframework.http.ResponseEntity;
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;

@RequestMapping("/auth")
@RestController
public class AuthenticationController {
private final JwtService jwtService;
private final AuthenticationService authenticationService;

public AuthenticationController(JwtService jwtService, AuthenticationService authenticatio


this.jwtService = jwtService;
this.authenticationService = authenticationService;
}

@PostMapping("/signup")
public ResponseEntity<User> register(@RequestBody RegisterUserDto registerUserDto) {
User registeredUser = authenticationService.signup(registerUserDto)

return ResponseEntity.ok(registeredUser);
}

@PostMapping("/login")
public ResponseEntity<LoginResponse> authenticate(@RequestBody LoginUserDto loginU
User authenticatedUser = authenticationService.authenticate(loginUse

String jwtToken = jwtService.generateToken(authenticatedUser);

LoginResponse loginResponse = new LoginResponse().setToken(jwtToken

return ResponseEntity.ok(loginResponse);
}
}

The authentication request returns a LoginReponse instance; below is


the code of that file:

public class LoginResponse {


private String token;
private long expiresIn;

public String getToken() {


return token;
}
// Getters and setters...
}

Test the implementation


Run the application, open an HTTP client, and send a POST request to
/auth/signup with the information in the request body.

Call the API route to register a new user.

Now, let's try to authenticate with the user we registered. Send a POST
request to /auth/login with the information in the request body.
Call the API route to authenticate a user and generate the JWT.

Create restricted endpoints to retrieve users


The endpoints /users/me and /users respectively return the
authenticated user from the JWT token provided and the list of all the
users.
Create the file UserController.java and add the code below:

package com.tericcabrel.authapi.controllers;

import com.tericcabrel.authapi.entities.User;
import com.tericcabrel.authapi.services.UserService;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RequestMapping("/users")
@RestController
public class UserController {
private final UserService userService;

public UserController(UserService userService) {


this.userService = userService;
}

@GetMapping("/me")
public ResponseEntity<User> authenticatedUser() {
Authentication authentication = SecurityContextHolder.getContext().g

User currentUser = (User) authentication.getPrincipal();

return ResponseEntity.ok(currentUser);
}

@GetMapping("/")
public ResponseEntity<List<User>> allUsers() {
List <User> users = userService.allUsers();

return ResponseEntity.ok(users);
}
}

In the above code, we retrieve the authenticated user from the security
context that has been set in the file JwtAuthenticationFilter.java
at line 68.
we can see the UserService is injected in the controller and provides
the function allUsers() that retrieves the list of users in the database
and returns it.
Before testing the implementation, let's create the UserService.java
in the package services and add the code below:

package com.tericcabrel.authapi.services;

import com.tericcabrel.authapi.entities.User;
import com.tericcabrel.authapi.repositories.UserRepository;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class UserService {
private final UserRepository userRepository;

public UserService(UserRepository userRepository) {


this.userRepository = userRepository;
}

public List<User> allUsers() {


List<User> users = new ArrayList<>();

userRepository.findAll().forEach(users::add);

return users;
}
}

Test the implementation


Re-run the application and follow this scenario:
1. Send a GET request to /users/me and /users , you will get a
403 error
2. Authenticate with POST request at /auth/login and obtain
the JWT token.
3. Put the JWT token in the authorization header of the request
/users/me and /users ; you will get an HTTP response code
200 with the data.

Call the protected API routes using the JWT generated through the login

Customize authentication error messages


The API prevents access from an unauthenticated user or returns a
status error when the authentication credentials aren't valid. Still, there
is no additional message to give more details to developers consuming
the API.
There is no response body when there is an authentication error.

There are different authentications we want to return a more explicit


message. The table below enumerates them:
AUTHENTICATION ERROR EXCEPTION THROWN HTTP STATUS CODE

Bad login credentials BadCredentialsException 401

Account locked AccountStatusException 403

Not authorized to access a resource AccessDeniedException 403

Invalid JWT SignatureException 401

JWT has expired ExpiredJwtException 401

To handle these errors, we must use the Spring global exception


handler to catch the exception thrown and customize the response to
send to the client.
Create a package then create a file named
exceptions
GlobalExceptionHandler.java and add the code below:

package com.tericcabrel.authapi.exceptions;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.security.SignatureException;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ProblemDetail;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AccountStatusException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ProblemDetail handleSecurityException(Exception exception) {
ProblemDetail errorDetail = null;

// TODO send this stack trace to an observability tool


exception.printStackTrace();

if (exception instanceof BadCredentialsException) {


errorDetail = ProblemDetail.forStatusAndDetail(HttpStatusCode.va
errorDetail.setProperty( "description", "The username or password is inco
return errorDetail;
}

if (exception instanceof AccountStatusException) {


errorDetail = ProblemDetail.forStatusAndDetail(HttpStatusCode.va
errorDetail.setProperty( "description", "The account is locked");
}

if (exception instanceof AccessDeniedException) {


errorDetail = ProblemDetail.forStatusAndDetail(HttpStatusCode.va
errorDetail.setProperty( "description", "You are not authorized to access t
}
if (exception instanceof SignatureException) {
errorDetail = ProblemDetail.forStatusAndDetail(HttpStatusCode.va
errorDetail.setProperty( "description", "The JWT signature is invalid");
}

if (exception instanceof ExpiredJwtException) {


errorDetail = ProblemDetail.forStatusAndDetail(HttpStatusCode.va
errorDetail.setProperty( "description", "The JWT token has expired");
}

if (errorDetail == null) {
errorDetail = ProblemDetail.forStatusAndDetail(HttpStatusCode.va
errorDetail.setProperty( "description", "Unknown internal server error.");
}

return errorDetail;
}
}

Re-run the application and try to authenticate with invalid credentials,


send a request with an expired JWT or an invalid JWT, etc...
Authentication errors response returned with detailed information.

💡 The JWT exceptions are caught by the global exception handler because we
used the Handler exception resolver in the file JwtAuthenticationFilter.java to
forward them. The other exceptions come from Spring security.

Wrap up
In this post, we saw how to implement the JSON Web Token
authentication in a Spring Boot application. Here are the main steps of
this process:
JWT authentication filter to extract and validate the token from
the request header.
Whitelist some API routes and protect those requiring a token.
Perform the authentication, generate the JWT, and set an
expiration time.
Use the JWT generated to access protected routes.
Catch authentication exceptions to customize the response
sent to the client.
With this implementation, you have the basis to protect your API, and
you can go a step further by implementing a Role-Based Access
Control (RBAC) that we will implement in the upcoming tutorial.
You can find the code source on the GitHub repository.
Follow me on Twitter or subscribe to my newsletter to avoid missing the
upcoming posts and the tips and tricks I occasionally share.
Join the Newsletter
Get updates about new articles, contents, coding tips and tricks.
Email Address Subscribe

No spam, no ads, no sales. Unsubscribe at any time.

Spring Boot 3 and Docker Implement Role-based


compose integration Access Control in Spring
This post shows how to take advantage of Boot 3
the Spring Boot integration Docker… This post shows how to implement a Role
Oct 20, 2023 6 min read Based Access Control in a Spring Boot 3…
Oct 15, 2023 10 min read
Teco Tutorials © 2023
Twitter GitHub LinkedIn
Powered by Ghost

You might also like