JWT Authentication and Authorization with Spring Boot 3 and Spring Security 6

Truong Bui
10 min readMay 11, 2023

During my journey of learning Spring Security, I had some thinking whether there are other developers who share a similar experience. Those who struggle to comprehend the inner workings of Spring Security components or those who seek to learn through demonstration code and aspire to develop their own projects based on that foundation, etc. Therefore, I made the decision to write this article, with the hope that it may be helpful in some way. 😃

This article aims to showcase a basic web application with APIs secured by Spring Security. Instead of delving deep into introducing Spring Security concepts, as they are readily available on the official website, I will focus on providing comprehensive explanations while constructing project components. At the end of the article, you will find a link to the GitHub repository containing the functional code for reference.

Let’s get started! 💪

Application Architecture

Scenario

  • User makes a request to the service, seeking to create an account.
  • A user submits a request to the service to authenticate their account.
  • An authenticated user sends a request to access resources.

Sign Up

Sign-Up flow diagram

The Sign-up process is made very simple. One notable component is the JwtService, a custom service utilized for handling JWT operations. Further implementation details can be found in the coding section below.

  1. The process starts when a user submits a request to the service. A user object is then generated from the request data, with the password being encoded using the PasswordEncoder.
  2. The user object is stored in the database using the UserRepository, which leverages Spring Data JPA.
  3. The JwtService is invoked to generate a JWT for the User object.
  4. The JWT is encapsulated within a JSON response and subsequently returned to the user.

It is important to remember that we must inform Spring about the specific password encoder utilized in the application, In this case, we are using PasswordEncoder. This information is necessary for Spring to properly authenticate users by decoding their passwords. I will elaborate on this when we configure the AuthenticationProvider in the coding section.

Sign In

Sign-in flow diagram
  1. The process begins when a user sends a sign-in request to the Service. An Authentication object called UsernamePasswordAuthenticationToken is then generated, using the provided username and password.
  2. The AuthenticationManager is responsible for authenticating the Authentication object, handling all necessary tasks. If the username or password is incorrect, an exception is thrown, and a response with HTTP Status 403 is returned to the user.
  3. After successful authentication, an attempt is made to retrieve the user from the database. If the user does not exist in the database, a response with HTTP Status 403 is sent to the user. However, since we have already passed step 2 (authentication), this step is not crucial, as the user should already be in the database.
  4. Once we have the user information, we call the JwtService to generate the JWT.
  5. The JWT is then encapsulated in a JSON response and returned to the user.

Two new concepts are introduced in this process, and I’ll provide a brief explanation for each.
1. UsernamePasswordAuthenticationToken: A type of Authentication object which can be created from a username and password that are submitted.
2. AuthenticationManager: Processes authentication object and will do all authentication jobs for us.

Resources Access

This process is secured by Spring Security, Let’s examine its flow as follows.

  1. The process starts when the user sends a request to the Service. The request is first intercepted by JwtAuthenticationFilter, which is a custom filter integrated into the SecurityFilterChain.
  2. As the API is secured, if the JWT is missing, a response with HTTP Status 403 is sent to the user.
  3. When an existing JWT is received, JwtService is called to extract the userEmail from the JWT. If the userEmail cannot be extracted, a response with HTTP Status 403 is sent to the user.
  4. If the userEmail can be extracted, it will be used to query the user’s authentication and authorization information via UserDetailsService.
  5. If the user’s authentication and authorization information does not exist in the database, a response with HTTP Status 403 is sent to the user.
  6. If the JWT is expired, a response with HTTP Status 403 is sent to the user.
  7. Upon successful authentication, the user’s details are encapsulated in a UsernamePasswordAuthenticationToken object and stored in the SecurityContextHolder.
  8. The Spring Security Authorization process is automatically invoked.
  9. The request is dispatched to the controller, and a successful JSON response is returned to the user.

This process is a little more tricky, it involves some new concepts. Let’s dive into them in more detail:
1. SecurityFilterChain: a filter chain which is capable of being matched against an HttpServletRequest. in order to decide whether it applies to that request.
2. SecurityContextHolder: is where Spring Security stores the details of who is authenticated. Spring Security uses that information for authorization.
3.
UserDetailsService: Service to fetch user-specific data.
4. Authorization Architecture

Source Code Demonstration

For some of the trickier parts, I’ll provide detailed explanations and reference links. However, for the remaining parts, you can find the relevant information on GitHub.

Create Demo Service as a Spring Boot project with the dependencies provided inside below POM file. I have named it as security.

https://github.com/buingoctruong/springboot3-springsecurity6-jwt/blob/master/pom.xml

Database configuration

H2 is a well-known in-memory database and a pretty good choice when we want to perform a fast proof of concept. Spring Boot provides a handy interaction with H2, we no need to do something like install database, set up schema, set up tables, populate data, etc. 😎

In order to connect to H2 database, we need to add corresponding properties to application.yaml file. (Detailed explanations on properties are included as comments)

spring:
jpa:
# Provide database platform that is being used
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
# New database is created when app starts and destroyed when app stops
ddl-auto: create-drop
# Show sql when spring data jpa performs query
show-sql: true
properties:
hibernate:
# Format queries
format_sql: true
datasource:
# URL connection to database (spring-security is database name)
url: jdbc:h2:mem:spring-security
# H2 SQL driver dependency
driver-class-name: org.h2.Driver
username: root
password: 12345

Full application.yaml file: https://github.com/buingoctruong/springboot3-springsecurity6-jwt/blob/master/src/main/resources/application.yaml

User Entity

Authentication and Authorization means things related to User. Let’s create our User class (we should keep User model simple for this demo, but the real-world User model can be complex)

public enum Role {
USER,
ADMIN
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "_user")
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String firstName;
private String lastName;
private String email;
private String password;
@Enumerated(EnumType.STRING)
private Role role;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority(role.name()));
}

@Override
public String getUsername() {
// email in our case
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;
}
}

When utilizing Spring Security for authentication and authorization in our application, user-specific data must be provided to Spring Security API and used during the authentication process. This user-specific data is encapsulated in the UserDetails object. UserDetails is an interface that includes various methods. To simplify integration with Spring Security, I’m implementing UserDetails interface, as demonstrated in the code snippet above. (For a more detailed explanation, refer to the security configuration section).

Explore additional information about UserDetails in this reference link: UserDetails

User Repository

https://github.com/buingoctruong/springboot3-springsecurity6-jwt/blob/master/src/main/java/com/truongbn/security/repository/UserRepository.java

User Service

public interface UserService {
UserDetailsService userDetailsService();
}
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
@Override
public UserDetailsService userDetailsService() {
return new UserDetailsService() {
@Override
public UserDetails loadUserByUsername(String username) {
return userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
};
}
}

UserDetailsService is an interface that retrieves the user’s authentication and authorization information. It has only one method loadUserByUsername(), which can be implemented to supply user information to Spring Security API. The DaoAuthenticationProvider utilizes this method to load the user information when performing the authentication process.

Jwt Service

public interface JwtService {
String extractUserName(String token);

String generateToken(UserDetails userDetails);

boolean isTokenValid(String token, UserDetails userDetails);
}
@Service
public class JwtServiceImpl implements JwtService {
@Value("${token.signing.key}")
private String jwtSigningKey;
@Override
public String extractUserName(String token) {
return extractClaim(token, Claims::getSubject);
}

@Override
public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}

@Override
public boolean isTokenValid(String token, UserDetails userDetails) {
final String userName = extractUserName(token);
return (userName.equals(userDetails.getUsername())) && !isTokenExpired(token);
}

private <T> T extractClaim(String token, Function<Claims, T> claimsResolvers) {
final Claims claims = extractAllClaims(token);
return claimsResolvers.apply(claims);
}

private String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
return Jwts.builder().setClaims(extraClaims).setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 24))
.signWith(getSigningKey(), SignatureAlgorithm.HS256).compact();
}

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(getSigningKey()).build().parseClaimsJws(token)
.getBody();
}

private Key getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(jwtSigningKey);
return Keys.hmacShaKeyFor(keyBytes);
}
}

For those unfamiliar with JWT (JSON Web Token), you can refer to this link to learn more about it: JSON Web Token.

Authentication Service

public interface AuthenticationService {
JwtAuthenticationResponse signup(SignUpRequest request);

JwtAuthenticationResponse signin(SigninRequest request);
}
@Service
@RequiredArgsConstructor
public class AuthenticationServiceImpl implements AuthenticationService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtService jwtService;
private final AuthenticationManager authenticationManager;
@Override
public JwtAuthenticationResponse signup(SignUpRequest request) {
var user = User.builder().firstName(request.getFirstName()).lastName(request.getLastName())
.email(request.getEmail()).password(passwordEncoder.encode(request.getPassword()))
.role(Role.USER).build();
userRepository.save(user);
var jwt = jwtService.generateToken(user);
return JwtAuthenticationResponse.builder().token(jwt).build();
}

@Override
public JwtAuthenticationResponse signin(SigninRequest request) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()));
var user = userRepository.findByEmail(request.getEmail())
.orElseThrow(() -> new IllegalArgumentException("Invalid email or password"));
var jwt = jwtService.generateToken(user);
return JwtAuthenticationResponse.builder().token(jwt).build();
}
}

For more details on the processes of Sign Up and Sign In, please refer to the diagrams provided in the Application Architecture section above.

Custom Filter

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserService userService;
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response, @NonNull FilterChain filterChain)
throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String userEmail;
if (StringUtils.isEmpty(authHeader) || !StringUtils.startsWith(authHeader, "Bearer ")) {
filterChain.doFilter(request, response);
return;
}
jwt = authHeader.substring(7);
userEmail = jwtService.extractUserName(jwt);
if (StringUtils.isNotEmpty(userEmail)
&& SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userService.userDetailsService()
.loadUserByUsername(userEmail);
if (jwtService.isTokenValid(jwt, userDetails)) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
context.setAuthentication(authToken);
SecurityContextHolder.setContext(context);
}
}
filterChain.doFilter(request, response);
}
}

The custom filter extendsOncePerRequestFilter to ensure that our filter is invoked only once for each request. It defines the following functionalities:

  • Retrieve the userEmail by parsing the Bearer Token and subsequently search for the corresponding user information in the database.
  • Verify the authenticity of the JWT.
  • Generate an Authentication object using the provided username and password, and subsequently store it in the SecurityContextHolder.

Refer to this link for detailed information about OncePerRequestFilter: OncePerRequestFilter

Web Security Setup

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final UserService userService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(request -> request.requestMatchers("/api/v1/auth/**")
.permitAll().anyRequest().authenticated())
.sessionManagement(manager -> manager.sessionCreationPolicy(STATELESS))
.authenticationProvider(authenticationProvider()).addFilterBefore(
jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}

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

@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userService.userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}

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

The @EnableWebSecurity annotation enables Spring web security and configures the following aspects:

  • Defining the bean authenticationProvider this one is used during the authentication process.
  • Defining the bean passwordEncoder spring will use when decoding passwords.
  • Defining authentication manager bean.
  • Defining Security filter chain bean. Set up some rules like
    - A while list requests {/api/v1/auth/**}, any other request should be authenticated.
    - Stateless management, which means we should not store the authentication state.
    - Add a type of data access object provider - DaoAuthenticationProviderwhich is responsible to fetch user info and encode/decode passwords.
  • Add JwtAuthenticationFilter before UsernamePasswordAuthenticationFilter because we extract username and password and then update them to SecurityContextHolder in JwtAuthenticationFilter

Two new concepts are introduced here. For more information, please refer to the provided reference links: DaoAuthenticationProvider, Usernamepasswordauthenticationfilter

Controller

https://github.com/buingoctruong/springboot3-springsecurity6-jwt/blob/master/src/main/java/com/truongbn/security/controller/AuthenticationController.java

https://github.com/buingoctruong/springboot3-springsecurity6-jwt/blob/master/src/main/java/com/truongbn/security/controller/AuthorizationController.java

Time to test what we did

Run the application (it should run on 8080), and open Postman.

  • Hit the following API: http://localhost:8080/api/v1/auth/signup. Remember to include the required request body. Afterwards, examine the response: a successful registration message along with the user token will be returned.
  • Hit the Sign-In API URL: http://localhost:8080/api/v1/auth/signin, ensuring that the request body contains an invalid password. Subsequently, examine the response: the authentication process will fail, and the HTTP status code will be 403.
  • Hit the Sign-In API UR again, this time including a valid password in the request body. Proceed to examine the response: the authentication process will be successful, and the user token will be returned.
  • Hit this http://localhost:8080/api/v1/resource without including the required authorization. As this API is protected, the resource cannot be accessed without a token. Verify the response for further details.
  • Copy the user token generated during the sign-up process and include it as an authorization header (Bearer Token type). Send another request to the Resource API URL and examine the response: we should now have successful access to the desired resource. 😍

We have just witnessed a brief demonstration of Spring Security in action, hope it’s working as expected you guys!

Is Spring Security challenging? Absolutely! It can be quite difficult to comprehend its inner workings initially. Personally, even after completing this article, I still haven’t grasped it entirely 😆. However, don’t give up and keep persevering 💪 . Remember, many individuals are willing to assist one another, so please don’t hesitate to share your thoughts or concerns in the comments section.

Completed source code can be found in this GitHub repository: https://github.com/buingoctruong/springboot3-springsecurity6-jwt

Happy learning!

Bye!

--

--