It can be said that this is the most difficult thing when learning Spring Boot, I have learned JWT authentication in PHP, the configuration is simple but Java is the opposite. But after all, I have already configured it successfully so I am writing this post so that note it for myself and if you are learning can refer to this post
I have a project that has an authentication that is so simple. You can see it in the following codes
@Service
public class UserService implements IUserService{
@Autowired
UserRepository userRepo;
@Autowired
UserConverter userConverter;
@Override
public AuthResponse login(UserDTO userDTO) {
//Check email
Optional<UserEntity> userData = userRepo.findByEmail(userDTO.getEmail());
if(userData.isPresent())
{
//Check password
BCryptPasswordEncoder bcrypt = new BCryptPasswordEncoder();
UserEntity entity = userData.get();
if(bcrypt.matches(userDTO.getPassword(), entity.getPassword() ))
{
return new AuthResponse(200, "Login is successful");
}
return new AuthResponse(401, "Password is wrong");
}
return new AuthResponse(401, "Password is wrong");
}
The login method above is so simple logic to authenticate a valid user. However, it has a big issue: after authentication is successful, we don't have any way to detect whether I have authentication before so JWT authentication will help to solve this issue
- I will destroy the old login method to build a new authentication using JWT authentication
The following are two necessary dependencies to build JWT Authentication
<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>
Of course, we need to have the necessary libraries of a project Spring Boot, for example, the following are all dependencies in my pom.xml file
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web-services</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<!--This dependency contains something like a Bcrypt password encoder,...-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<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>
You create a JwtAuthenticationEntryPoint.java file in config package:
import java.io.IOException;
import java.io.Serializable;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
/**
* This class will extend Spring's AuthenticationEntryPoint class and override its method commence.
* It rejects every unauthenticated request and send error code 401
*/
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
/**
* This is a unique identifier for serializing and deserializing objects. It ensures
* version compatibility during serialization.
* */
private static final long serialVersionUID = -7858869558953243875L;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
The commence Method Override is part of the AuthenticationEntryPoint interface and is overridden here.
Overall, this class ensures that any unauthenticated request made to the application receives a proper HTTP 401 response, indicating that the request is unauthorized.
For example, when I call API to log in with the email and password, but if I send the wrong password to the server, the commence method will be called and throw the error with the result like this:
{
"timestamp": 1712757015627,
"status": 401,
"error": "Unauthorized",
"exception": "java.lang.Exception",
"message": "Unauthorized",
"path": "/login"
}
You create a MyUserDetails class that implements UserDetails interface provided by Spring Security
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
public class MyUserDetails implements UserDetails {
private String password;
private String username;
private List<GrantedAuthority> authorities;
public MyUserDetails(String username, String password, Long idUT){
this.username = username;
this.password = password;
this.authorities = translateAuthorities(idUT);
}
private List<GrantedAuthority> translateAuthorities(Long idUT) {
List<GrantedAuthority> list = new ArrayList<>();
if(idUT == 1)
list.add(new SimpleGrantedAuthority("ROLE_USER"));
else if(idUT == 2)
list.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
else if(idUT == 3)
list.add(new SimpleGrantedAuthority("ROLE_SELLER"));
return list;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
The explanation for this class in section 9
You create a JwtTokenUtils.java file inside the config package:
import java.io.Serializable;
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.stereotype.Component;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
/*
The JwtTokenUtil is responsible for performing JWT operations like creation and validation.
It makes use of the io.jsonwebtoken.Jwts for achieving this.
*/
@Component
public class JwtTokenUtils implements Serializable {
/** This is a unique identifier for serializing and deserializing objects. */
private static final long serialVersionUID = -2550185165626007488L;
/** This constant defines the validity period of JWT tokens. */
public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60;
/**The secret key used for JWT generation and validation is retrieved from Spring properties. */
@Value("${jwt.secret}")
private String secret;
//retrieve username from jwt token
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
//retrieve expiration date from jwt token
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
/** Generic method to retrieve any claim from a JWT token. */
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
//for retrieveing any information from token we will need the secret key
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
//check if the token has expired
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
//generate token for user
public String generateToken(MyUserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return doGenerateToken(claims, userDetails.getUsername());
}
//while creating the token -
//1. Define claims of the token, like Issuer, Expiration, Subject, and the ID
//2. Sign the JWT using the HS512 algorithm and secret key.
//3. According to JWS Compact Serialization(https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-3.1)
// compaction of the JWT to a URL-safe string
private String doGenerateToken(Map<String, Object> claims, String subject) {
return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
.signWith(SignatureAlgorithm.HS512, secret).compact();
}
//validate token
public Boolean validateToken(String token, MyUserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
Overall, this class encapsulates the logic for JWT token handling, including generation, validation, and extraction of information from tokens, providing a secure way to manage authentication and authorization in a Spring application.
You create a MyUserDetailsService.java file inside the service package:
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
UserRepository userRepo;
@Override
public MyUserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Optional<UserEntity> userData = userRepo.findByEmail(email);
if(userData.isPresent())
return new MyUserDetails(userData.get().getEmail(), userData.get().getPassword(), userData.get().getIdUT());
throw new UsernameNotFoundException("User not found with username: " + email);
}
}
Because in my system, to look up a user, this must be given an email to look up so we need to create the MyUserDetailsService implements UserDetailsService provided by Spring Security to Overwrite the loadUserByUsername method to tell it that: "You will search for a user by email"
Overall, the MyUserDetailsService class serves as a bridge between Spring Security's authentication mechanism and the user data stored in the application's database. It retrieves user details based on the provided email and constructs a UserDetails object, which is used by Spring Security for authentication and authorization.
We create a JwtTokenFilter.java, you can create this file inside the filter package
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@Component
public class JwtTokenFilter extends OncePerRequestFilter{
@Autowired
private MyUserDetailsService userDetailsSerive;
@Autowired
private JwtTokenUtils jwtTokenUtils;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
if(authHeader != null && authHeader.startsWith("Bearer "))
{
final String token = authHeader.substring(7);
final String email = jwtTokenUtils.getUsernameFromToken(token);
if(email != null && SecurityContextHolder.getContext().getAuthentication() == null)
{
MyUserDetails userDetails = userDetailsSerive.loadUserByUsername(email);
if(jwtTokenUtils.validateToken(token, userDetails))
{
UsernamePasswordAuthenticationToken upat
= new UsernamePasswordAuthenticationToken(userDetails,null, userDetails.getAuthorities());
// After setting the Authentication in the context, we specify
// that the current user is authenticated. So it passes the
// Spring Security Configurations successfully.
SecurityContextHolder.getContext().setAuthentication(upat);
}
}
}
filterChain.doFilter(request, response);
}
}
Overall, the JwtTokenFilter class acts as a middleware in the Spring Security filter chain, intercepting requests to extract and validate JWT tokens for authentication. It integrates with Spring Security's authentication mechanism by setting the authentication token in the security context, allowing subsequent filters and components to access the authenticated user's details.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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;
/**
* The class is annotated with @Configuration, indicating that it's a configuration class.
* @EnableWebSecurity enables Spring Security's web security support.
* @EnableGlobalMethodSecurity(prePostEnabled = true) enables method-level security with pre and post annotations.
* */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
private UserDetailsService jwtUserDetailsService;
@Autowired
private JwtTokenFilter jwtRequestFilter;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// configure AuthenticationManager so that it knows from where to load
// .. user for matching credentials
// Use BCryptPasswordEncoder
auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// We don't need CSRF for this example
httpSecurity.csrf().disable()
// dont authenticate this particular request
.authorizeRequests().antMatchers("/login", "/register").permitAll().
// all other requests need to be authenticated
anyRequest().authenticated().and().
// make sure we use stateless session; session won't be used to
// store user's state.
exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// Add a filter to validate the tokens with every request
httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
}
Overall, this class provides comprehensive security configurations for the web application, including authentication, authorization, and session management, using Spring Security features.
From the sections 1, 2, 3, 4, 5, 6, 7. The system can create a JWT token when a user logs in.
For example, I will create a controller named UserController.java:
@CrossOrigin
@RestController
public class UserController {
@Autowired
IUserService userService;
@Autowired
MyUserDetailsService userDetailsService;
@Autowired
AuthenticationManager authenticationManager;
@Autowired
JwtTokenUtils jwtTokenUtils;
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@RequestBody UserDTO userDTO) throws Exception
{
authenticate(userDTO.getEmail(), userDTO.getPassword());
final MyUserDetails userDetails = userDetailsService.loadUserByUsername(userDTO.getEmail());
final String token = jwtTokenUtils.generateToken(userDetails);
final UserDTO userInfo = userService.get(userDTO);
return ResponseEntity.ok(new AuthResponse(200, "Login is success", userInfo, token));
}
private void authenticate(String email, String password) throws Exception {
try {
// Load user details by email
MyUserDetails userDetails = userDetailsService.loadUserByUsername(email);
// Authenticate using email and password
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(email, password, userDetails.getAuthorities()));
} catch (AuthenticationException e) {
// Authentication failed
throw new Exception("Invalid credentials", e);
}
}
}
The tọken was returned will be used to authenticate for the next steps
In the section before, we were given a token after authenticating successfully, We will use this token for each call API that requires authentication.
For example, in UserController, I have a getAll method to get a user list:
@GetMapping("/user")
public List<UserDTO> getAll()
{
return userService.getAll();
}
If you call an API with GET method with endpoint /user to the server but without providing the JWT token given by login before you will encounter the issue about authentication.
To solve this problem, you must set the token to the request, in the server will get this token to extract the claim (subject), it had been set at generateToken method in JwtTokenUtils before.
In sections 1 - 9, we have built a JWT authentication system.
I will explain in detail the section 3 MyUserDetails class
The MyUserDetails class same as the default User class of Spring Security, but it only updates the way assigned to this.authorities field.
The constructor will get a 3rd parameter idUT (id of user type entity), with:
For example, if I expect only users to have the role "admin" to access the endpoint "/user", on the opposite, the requests will be blocked, it is so simple, that we will use @PreAuthorize annotation for any method in the controllers.
In my case, I want the getAll method only accessed by the admin role:
@GetMapping("/user")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public List<UserDTO> getAll()
{
return userService.getAll();
}
If a user with the role "user" accesses this endpoint it will receive an error 403 Forbidden:
{
"timestamp": 1712821104371,
"status": 403,
"error": "Forbidden",
"exception": "org.springframework.security.access.AccessDeniedException",
"message": "Access is denied",
"path": "/user"
}
For example, when a request goes through the filter and is authenticated. It will jump to the controller if we get the email that is extracted from the filter before, can we do this... Yes! we can do it by passing a parameter with the Authentication type provided by Spring Security like the following:
@PostMapping("/authenticate")
public ResponseEntity<AuthResponse> authenticate(Authentication authentication)
{
String email = authentication.getName(); // '[email protected]'
}
Or you can use Principal:
import java.security.Principal;
@PostMapping("/authenticate")
public ResponseEntity<AuthResponse> authenticate(Principal principal)
{
String email = principal.getName(); // '[email protected]'
}
Or you can get the username and password and more with Authentication:
@PostMapping("/authenticate")
public ResponseEntity<AuthResponse> authenticate(Authentication authentication)
{
MyUserDetails mUser = (MyUserDetails) authentication.getPrincipal();
String email = mUser.getUsername(); // '[email protected]'
String passsword = mUser.getPassword(); // eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJtYXJ5QHlhaG9vLmNvbSIsImV4cCI6MTcxMjg1NTQzMCwiaWF0IjoxNzEyODM3NDMwfQ.vdR3P3DMASSidSdXkfhEzExjEY0hfYUTdR6wFSrUElP2v8ozHswyAf9cyPeGKifTJn3PQL74G8RnC6qDqFTcQQ
}
We have learned How to build a JWT authentication system for my application.