LazyCodet

a

12:06:03 10/4/2024 - 26 views -
Programming

JWT Authentication in Spring Security


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

1. POM Configuration

​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>

2. JwtAuthenticationEntryPoint

​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.

  • It's called for every unauthenticated request that reaches the application.
  • Parameters:
    • HttpServletRequest request: Represents the HTTP request made by the client.
    • HttpServletResponse response: Represents the HTTP response to be sent back to the client.
    • AuthenticationException authException: Represents the authentication exception that caused the entry point to be invoked.

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"
}

3. MyUserDetails

​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

4. JwtTokenUtils

​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.

5. MyUserDetailsService

​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.

​6. JwtTokenFilter

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);
	}
}
  • doFilterInternal Method:
    • This method is invoked for each HTTP request.
    • It intercepts the request and checks if it contains an Authorization header with a JWT token (Bearer scheme).
    • If a valid token is found, it extracts the email from the token using jwtTokenUtils.getUsernameFromToken.
    • It then attempts to load user details (UserDetails) from the MyUserDetailsService based on the extracted email.
    • If the token is valid and the user details are successfully loaded, it validates the token using jwtTokenUtils.validateToken.
    • If the token is valid, it constructs a UsernamePasswordAuthenticationToken with the user details and sets it in the security context using SecurityContextHolder.getContext().setAuthentication(upat).
    • The request is then passed down the filter chain using 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.

7. WebSecurityConfig

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.

​8. Create a token when login successful

​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);
        }
    }
}
  1. ​When having an API request with endpoint "/login", the login method will be called
  2. After that, the authenticate method will be executed to authenticate the information's user using the email and password provided
  3. If the information's user is authenticated, it will run the next line in the login method but on the opposite, it will throw an AuthenticationException error.
  4. If authenticated, we will create a JWT token from userDetails
  5. Finally, We return a response to the client.

​The tọken was returned will be used to authenticate for the next steps

​9. Token Authentication

​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. 

​10. Authorities & @PreAuthorize

​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:

  • ​1 = user
  • 2 = admin
  • 3 = seller

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"
}

​11. Get Authentication info in the controllers

​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
	
}	

​Conclusion

​We have learned How to build a JWT authentication system for my application.

​References

​https://youtu.be/cXPEWom3ChQ

​https://youtu.be/NSFLGrM6pAU

​https://codersontrang.wordpress.com/2013/09/03/userdetails-userdetailsservice-trong-spring-security/