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:
- User SignUp: Registering a new user.
- User Login: Authenticating the user.
- Token Generation: Creating a JWT after successful login.
- Token Structure: Understanding the composition of a JWT.
- Client-Side Storage: Storing the token on the client side.
- Authenticated Requests: Using the token in requests to access protected resources.
- Token Validation: Ensuring the token’s authenticity.
- Token Expiration: Managing token lifespan.
- Security Considerations: Ensuring secure token usage and storage.
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
.
- 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);
}
}
- 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();
}
}
- 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));
}
}
- 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.