Spring Boot HandBook

    Introduction :#

    JWT (JSON Web Token) is a widely used method for securely transmitting information between parties in a JSON format, typically for authenticating and authorizing web application requests. The JWT authentication process involves the following steps:

    1. User SignUp: Registering a new user.
    2. User Login: Authenticating the user.
    3. Token Generation: Creating a JWT after successful login.
    4. Token Structure: Understanding the composition of a JWT.
    5. Client-Side Storage: Storing the token on the client side.
    6. Authenticated Requests: Using the token in requests to access protected resources.
    7. Token Validation: Ensuring the token’s authenticity.
    8. Token Expiration: Managing token lifespan.
    9. Security Considerations: Ensuring secure token usage and storage.

    Implementation in Code:#

    Authenticate Future Requests with JWT: After login, the user receives a JWT, which is included in the Authorization header of all subsequent requests. The server uses this token to authenticate each request.

    Authenticate Requests Containing a Token: Every incoming request must be checked for a valid JWT. If a valid token is present, the server authenticates the user based on it.

    Check if the Token is Valid: The server validates the JWT by checking its signature, expiration, and other claims.

    Pass the Authentication Object to Spring Security Context Holder: If the token is valid, an Authentication object is created and stored in the SecurityContextHolder, making the authenticated user available across the application (e.g., in controllers).

    Implementation Steps

    • The user sends a POST request with their credentials to the /authenticate endpoint.
    • If valid, a JWT is generated and returned to the client.
    • The client includes the JWT in the Authorization header (as Bearer <token>).
    • The JwtRequestFilter intercepts the request, validates the token, and sets the authentication object in the SecurityContextHolder if the token is valid.
    • The request proceeds to the controller, which can now access the authenticated user's details via the SecurityContextHolder.
    1. JWT Request Filter:
    import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; @Component @RequiredArgsConstructor public class JWTAuthFilter extends OncePerRequestFilter { private final JWTService jwtService; private final UserServiceImpl userService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { final String requestTokenHeader = request.getHeader("Authorization"); //Extract the Authorization Header: //this token is actually concatinated with Bearer space("Bearer ") //Check for a Bearer Token: if(requestTokenHeader == null || !requestTokenHeader.startsWith("Bearer ")){ filterChain.doFilter(request,response); return; } //Ensure that the token in the header is correctly formatted as "Bearer <your-jwt-token>". String token = requestTokenHeader.split("Bearer ")[1]; // Extract JWT token from the header Long userId = jwtService.generateUserIdFromToken(token); //generate the token //Check if userId is Present and the Security Context is Empty if(userId != null && SecurityContextHolder.getContext().getAuthentication() == null) { //Retrieve the User Entity UserEntity user = userService.getUserById(userId); //Create an Authentication Token UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, null); //put the user in spring security context holder and in other field we used 'null' for only testing purpose //Set Authentication Details authenticationToken.setDetails( new WebAuthenticationDetailsSource().buildDetails(request) ); //Set Authentication in Security Context SecurityContextHolder.getContext().setAuthentication(authenticationToken); } //Continue the Filter Chain filterChain.doFilter(request,response); } }
    1. Security Configuration:

    This configuration sets up the security filter chain, making sure that requests are processed by the JWTAuthFilter before reaching the authentication filter.

    Configure Spring Security to use the JWT filter:

    import lombok.RequiredArgsConstructor; 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.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity //by adding this annotation we are telling springboot that we want to we want to configure this SpringSecurityFilterChain @RequiredArgsConstructor public class WebSecurityConfig { private final JWTAuthFilter jwtAuthFilter; @Bean SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { httpSecurity .authorizeHttpRequests(auth-> auth .requestMatchers("/endpoint","/error","/auth/**").permitAll() // Public endpoints .anyRequest().authenticated()) // All other requests require authentication .csrf(csrfConfig-> // Disable CSRF for simplicity, not recommended for production csrfConfig .disable()) .sessionManagement(sessionConfig -> // Disable JSESSIONID for simplicity, not recommended for production sessionConfig .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); //add the filter just before of the ther usernamepasswordauthenticationfilter return httpSecurity.build(); //when you add build this throws an exception } //create a authenticationmanager bean @Bean AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } //create a passwordencoder bean @Bean PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
    1. UserService:

    This service provides methods for loading user details by username and fetching user entities by ID. Here you can use email also.

    The UserService loads user details based on the user ID extracted from the JWT:

    import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class UserService implements UserDetailsService { private final UserRepo userRepo; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return userRepo .findByEmail(username) // .orElse(null); //or you can throw an exception here .orElseThrow(()->new ResourceNotFoundException("USER WITH EMAIL "+username+" NOT FOUND")); } public UserEntity getUserById(Long userId){ return userRepo.findById(userId).orElseThrow(()->new ResourceNotFoundException("USER ID NOT FOUND: "+userId)); } }
    1. Create Some Custom Controller, Dto, Entity, Repo, Service class to test all the request :

    Finally, create a controller and necessary DTOs, entities, repositories, and services to test this setup.

    In this example, you can use curl or Postman to send requests with a valid JWT token in the Authorization header to /user/profile and receive the user's profile information.

    import lombok.Data; @Data public class DemoDto { private Long id; private String name; } import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import lombok.Getter; import lombok.Setter; @Entity @Getter @Setter public class DemoEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; } import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequiredArgsConstructor @RequestMapping("/endpoint") public class DemoController { private final DemoService demoService; @PostMapping public ResponseEntity<DemoDto> createNewDemo(@RequestBody DemoDto demoDto){ return ResponseEntity.ok(demoService.createNewDemo(demoDto)); } @GetMapping public ResponseEntity<List<DemoDto>> getAllDemo(){ return ResponseEntity.ok(demoService.getAllDemo()); } @GetMapping("/{id}") public ResponseEntity<DemoDto> getDemoById(@PathVariable Long id){ return ResponseEntity.ok(demoService.getDemoById(id)); } } import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface DemoRepo extends JpaRepository<DemoEntity,Long> { } import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.modelmapper.ModelMapper; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import java.util.List; import java.util.stream.Collectors; @Service @RequiredArgsConstructor @Slf4j public class DemoService { private final DemoRepo demoRepo; private final ModelMapper modelMapper; public DemoDto createNewDemo(DemoDto demoDto) { DemoEntity demo = modelMapper.map(demoDto,DemoEntity.class); DemoEntity savedDemo = demoRepo.save(demo); return modelMapper.map(savedDemo,DemoDto.class); } public List<DemoDto> getAllDemo() { return demoRepo.findAll() .stream() .map(demoEntity -> modelMapper.map(demoEntity,DemoDto.class)) .collect(Collectors.toList()); } public DemoDto getDemoById(Long id) { //here if you want then we can get the authentication from the spring security context holder UserEntity user = (UserEntity) SecurityContextHolder.getContext() .getAuthentication() .getPrincipal(); // this principal contains the user details only because in filter we use the 'user' object directly log.info("user {}",user); //logging our logged in user here DemoEntity demo = demoRepo.findById(id) .orElseThrow(()->new ResourceNotFoundException("Demo id not found: "+id)); return modelMapper.map(demo,DemoDto.class); } }

    Output:

    If the Authorization header is missing or doesn't start with "Bearer ", the JWT authentication process is skipped entirely, leading to the request being treated as unauthenticated, which could result in a 403 Forbidden error if the endpoint requires authentication.

    When a user successfully logs in, the server generates a JWT and returns it to the client. This JWT is then used by the client in subsequent requests by adding it to the Authorization header as Bearer <token>. For example, if the login is successful, the response might look like:

    When the client sends a request to a protected endpoint (e.g., /endpoint/1) with the JWT in the Authorization here, we are getting “404 Not Found” because we do not have any ‘id 1’. Basically here with the help of the valid token this request is working. After 1min if we want to send the same request it will give us the “500 Internal Server” error.

    We did not handle the exception here in our code till now. So, if you go to your ide run console you can see that the “ExpiredJwtException” that JWT expired.

    Last updated on Dec 09, 2024