In web applications, security, especially authentication and authorization processes, plays a critical role. In this article, I will explain in detail how to perform user authentication using JWT (JSON Web Token) in a Spring Boot application.
1. Adding Required Dependencies #
To perform JWT-based authentication and authorization in a Spring Boot application, you need to add some essential dependencies to your project. These dependencies are necessary for security and JWT operations. Below is how to add these dependencies to your Spring Boot project.
If you are using Maven:
You can include the required libraries in your pom.xml
file by adding the following dependencies.
<dependencies>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JJWT (Java JWT) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
</dependencies>
If you are using Gradle:
If you are using Gradle, you can add the following dependencies to your build.gradle
file.
dependencies {
// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'
// JJWT (Java JWT) API, Implementation, and Jackson support
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}
2. Storing User Information Securely: AuthDto
#
As the first step, we need to create a DTO (Data Transfer Object) class to store user information. This class will represent the information that will be included in the JWT token. For example, it might include user ID, email address, and roles:
@Builder
@Getter
@Setter
public class AuthDto {
private UUID id;
private String email;
private List<GrantedAuthority> roles;
}
This DTO will carry the user information and will be used securely within the SecurityContext
.
3. JWT Creation and Validation: TokenService
#
We need to write a service class that will handle the creation and validation of the JWT. This class will manage all operations from generating the token to validation and constructing the authentication context.
@RequiredArgsConstructor
@Service
public class TokenService implements ITokenService {
private final IUserRepository userRepository;
private final int MILLISECOND_IN_MINUTE = 1000 * 60; // Milliseconds per minute
private final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256; // Signing algorithm
@Value("${security.token.access.secret}")
private String accessTokenSecret; // Secret key for Access token
@Value("${security.token.access.expires}")
private int accessTokenExpires; // Expiration time for Access token (in minutes)
// Generating JWT from User object
@Override
public String generateAccessToken(User user) {
Date now = new Date(); // Current time
int expiresIn = accessTokenExpires * MILLISECOND_IN_MINUTE; // Expiration time in milliseconds
// Create JWT token
return Jwts.builder()
.setSubject(user.getEmail()) // User's email
.claim("id", user.getId()) // User's ID
.claim("roles", rolesToString(user.getRoles())) // User's roles as a string
.setIssuedAt(now) // Token issued at
.setExpiration(new Date(now.getTime() + expiresIn)) // Token expiration time
.signWith(jwtKeyGenerator(accessTokenSecret), SIGNATURE_ALGORITHM) // JWT signing
.compact(); // Generate the token
}
// Generating authentication from JWT token
@Override
public Authentication getAuthenticationFromAccessToken(String accessToken) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(jwtKeyGenerator(accessTokenSecret)) // Verify the JWT signature using the secret key
.build().parseClaimsJws(accessToken).getBody(); // Parse the JWT token
// Get roles from token and convert to GrantedAuthority
List<GrantedAuthority> roles = stringToRoles(claims.get("roles", String.class));
// Build Authentication object with user information
AuthDto authDto = AuthDto.builder()
.id(UUID.fromString(claims.get("id", String.class))) // User's UUID
.email(claims.getSubject()) // User's email
.roles(roles) // User's roles
.build();
// Return Authentication object
return new UsernamePasswordAuthenticationToken(authDto, null, roles);
}
// Convert roles to a comma-separated string
private String rolesToString(Collection<Role> roles) {
return roles.stream()
.map(Role::name) // Get role names
.collect(Collectors.joining(",")); // Join with commas
}
// Convert string roles to GrantedAuthority list
private List<GrantedAuthority> stringToRoles(String rolesString) {
if (rolesString == null || rolesString.isEmpty()) {
return Collections.emptyList(); // Return empty list if no roles
}
// Convert string roles to GrantedAuthority
return Arrays.stream(rolesString.split(",\\s*"))
.map(role -> Role.valueOf(role)) // Convert to Role enum
.filter(Objects::nonNull) // Filter non-null roles
.map(role -> new SimpleGrantedAuthority(role.name())) // Convert to GrantedAuthority
.collect(Collectors.toList());
}
// Generate secret key for JWT
private Key jwtKeyGenerator(String secret) {
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret)); // Decode the secret key with BASE64
}
}
4. JWT Validation with AuthTokenFilter
#
To check the validity of the JWT on every incoming request, we use a filter. This filter verifies the JWT, and if valid, authenticates the user.
@RequiredArgsConstructor
@Component
public class AuthTokenFilter extends OncePerRequestFilter {
private final ITokenService tokenService; // TokenService used for JWT verification
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String url = request.getRequestURI(); // Get the URL of the incoming request
// Skip JWT verification for '/api/auth/*' paths
if (url.matches("/api/auth/*")) {
filterChain.doFilter(request, response);
return;
}
// Get access token from cookie
Cookie accessTokenCookie = WebUtils.getCookie(request, "accessToken");
// If no token found, attach error and proceed
if (accessTokenCookie == null) {
attachError(request, ErrorCode.ACCESS_TOKEN_DOES_NOT_EXIST); // Attach error message
filterChain.doFilter(request, response);
return;
}
try {
// Validate the token and get Authentication object
Authentication authentication = tokenService.getAuthenticationFromAccessToken(accessTokenCookie.getValue());
// Set the authentication context
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (ExpiredJwtException e) {
// Handle expired token error
attachErrorAndClearAuthContext(request, ErrorCode.ACCESS_TOKEN_EXPIRED);
} catch (MalformedJwtException e) {
// Handle malformed token error
attachErrorAndClearAuthContext(request, ErrorCode.ACCESS_TOKEN_MALFORMED);
} catch (RuntimeException e) {
// Handle other errors
attachErrorAndClearAuthContext(request, ErrorCode.INTERNAL_SERVER);
}
filterChain.doFilter(request, response); // Continue with the filter chain
}
private void attachError(HttpServletRequest request, ErrorCode errorCode) {
// Attach error information to the request
request.setAttribute("exceptionStatus", errorCode.getHttpStatusCode());
request.setAttribute("exceptionCode", errorCode.getCode());
request.setAttribute("exceptionMessage", errorCode.getMessage());
}
private void attachErrorAndClearAuthContext(HttpServletRequest request, ErrorCode errorCode) {
// Attach error and clear authentication context
attachError(request, errorCode);
SecurityContextHolder.clearContext(); // Clear authentication information
}
}
5. Handling JWT Errors with AuthExceptionHandler
#
The AuthenticationEntryPoint
is used to manage errors during JWT verification, sending the correct error messages to users.
@Slf4j
@Component
public class AuthExceptionHandler implements AuthenticationEntryPoint {
final ObjectMapper MAPPER = new ObjectMapper(); // ObjectMapper for JSON responses
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
log.error("Unauthorized error: {}", authException.getMessage()); // Log error message
response.setContentType(MediaType.APPLICATION_JSON_VALUE); // Set response type to JSON
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // Set HTTP 401 (Unauthorized) status code
final Map<String, Object> body = new HashMap<>(); // Map to store error details
// If error details are present in the request, return them in the JSON response
if (request.getAttribute("exceptionCode") != null &&
request.getAttribute("exceptionMessage") != null &&
request.getAttribute("exceptionStatus") != null) {
body.put("code", request.getAttribute("exceptionCode").toString());
body.put("message", request.getAttribute("exceptionMessage").toString());
body.put("status", Integer.parseInt(request.getAttribute("exceptionStatus").toString()));
} else {
// If no error details are found, send a general authentication required error
body.put("code", ErrorCode.AUTHENTICATION_REQUIRED.getCode());
body.put("message", ErrorCode.AUTHENTICATION_REQUIRED.getMessage());
body.put("status", ErrorCode.AUTHENTICATION_REQUIRED.getHttpStatusCode());
}
// Send JSON response
MAPPER.writeValue(response.getOutputStream(), body);
}
}
6. Spring Security Configuration: SecurityConfig
#
Finally, we include the JWT validation filter and error handler in the Spring Security configuration. This configuration will handle security and validation processes across the application.
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final AuthTokenFilter authTokenFilter; // Filter for JWT validation
private final AuthExceptionHandler authExceptionHandler; // Error handler for JWT errors
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.exceptionHandling(customizer -> customizer.authenticationEntryPoint(authExceptionHandler)) // Use the error handler
.addFilterBefore(authTokenFilter, BasicAuthenticationFilter.class) // Add JWT validation filter
.csrf(AbstractHttpConfigurer::disable) // Disable CSRF protection
.sessionManagement(customizer -> customizer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // Stateless session management
.authorizeHttpRequests(requests -> requests
.requestMatchers("/api/auth/*").permitAll() // Allow unauthenticated access to auth endpoints
.anyRequest().authenticated()) // Require authentication for all other requests
.build(); // Apply configuration
}
}
Wrapping Up #
In this article, we have explored how to implement JWT-based authentication and authorization in Spring Boot applications. We covered key concepts like creating and validating JWT tokens, security filters, and error handling. If you have any questions, feel free to contact me.