Photo by King's Church International on Unsplash
JWT Authentication & Authorization in a Spring Boot 3 application
In this blog, we will explore the concept of JSON Web Tokens (JWT) and how they can be effectively utilized for authentication and authorization in a Spring Boot 3 application. JWTs provide a secure way to transmit information between parties, enabling stateless communication and reducing the need for session management on the server side. We will cover the fundamental principles of JWT, its structure, and how it enhances security in web applications.
Additionally, we will walk through the implementation process step-by-step, starting from setting up a new Spring Boot application to securing endpoints using Spring Security. We will discuss the necessary dependencies, create utility classes for token generation and validation, and implement authentication flows. By the end of this blog, you will have a clear understanding of how to integrate JWT into your Spring Boot applications, ensuring secure user authentication and authorization.
What is JWT?
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way to securely transmit information between parties as a JSON object. JWTs are commonly used for authentication and information exchange in web applications. A JWT typically consists of three parts: the header, the payload, and the signature.
Header: Contains metadata about the token, such as the type (JWT) and the signing algorithm (e.g., HMAC SHA-256).
Payload: Contains the claims or information about the user, such as user ID, roles, and expiration time.
Signature: Ensures the integrity and authenticity of the token.
What is JWT Signature?
The JWT signature is a crucial component that verifies the integrity and authenticity of the token. It is created by taking the encoded header and payload, then signing them with a secret key or a public/private key pair using a hashing algorithm.
The signature serves two main purposes:
Integrity: It ensures that the token has not been altered. If the signature is valid upon verification, it confirms that the header and payload remain unchanged.
Authentication: It verifies the identity of the sender. Only the issuer of the token (who has the secret key) can generate a valid signature, preventing unauthorized parties from creating their own tokens.
In summary, the JWT signature is essential for maintaining secure authentication and authorization in applications, protecting both the token's integrity and the user's identity.
Prerequisites
Before we dive into the implementation, ensure you have the following:
Java Development Kit (JDK) 17 or higher
Maven or Gradle for dependency management
Basic knowledge of Spring Boot and RESTful APIs
An IDE (like IntelliJ IDEA or Eclipse) for coding
Database
Implementation Steps
check below pom.xml and add spring Web, Spring Security, Spring Data JPA, MySQL Driver and JSON Web Token dependencies.
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.1</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.hk</groupId> <artifactId>springSecurity-boot3.x</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springSecurity-boot3.x</name> <description>Demo project for Spring Boot</description> <url/> <licenses> <license/> </licenses> <developers> <developer/> </developers> <scm> <connection/> <developerConnection/> <tag/> <url/> </scm> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <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> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
an user entity implementing "org.springframework.security.core.userdetails.UserDetails" and override required methods.
package com.hk.sec.prep.entity; 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; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.Data; @Data @Entity @Table(name="user") public class User implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer uId; private String firstName; private String lastName; private String email; private String password; private Role role; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return List.of(new SimpleGrantedAuthority(role.name())); } @Override public String getUsername() { return this.email; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
an service class with a method returning an object of "org.springframework.security.core.userdetails.UserDetailsService" and override required methods.
package com.hk.sec.prep.services; import org.springframework.security.core.userdetails.UserDetailsService; public interface UserService { public UserDetailsService userDetailsService(); }
package com.hk.sec.prep.services.impl; import org.springframework.beans.factory.annotation.Autowired; 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; import com.hk.sec.prep.repository.UserRepo; import com.hk.sec.prep.services.UserService; import lombok.RequiredArgsConstructor; @Service @RequiredArgsConstructor public class UserServiceImpl implements UserService{ @Autowired private UserRepo userRepo; @Override public UserDetailsService userDetailsService() { return new UserDetailsService() { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return userRepo.findByEmail(username).orElseThrow(() -> new UsernameNotFoundException("User Not Found")); } }; } }
create a JWT service class to generate token ,validate token and extract user name from token using a secret key (confidential string used in the signing process of a JWT to ensure that only authorized parties can create and verify the token).
package com.hk.sec.prep.services.impl; import java.security.Key; import java.util.Date; import java.util.Map; import java.util.function.Function; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; import com.hk.sec.prep.services.JWTService; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; @Service public class JWTServiceImpl implements JWTService { @Value("${security.jwt.secret-key}") private String secretKey; public String generateToken(UserDetails userDetails) { return Jwts.builder().setSubject(userDetails.getUsername()).setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + (1000 * 60 * 5))) .signWith(getSignKey(), SignatureAlgorithm.HS256).compact(); } public String generateRefreshToken(Map<String,Object> extraClaims, UserDetails userDetails) { return Jwts.builder().setClaims(extraClaims) .setSubject(userDetails.getUsername()) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + 604800000)) // 30 days .signWith(getSignKey(), SignatureAlgorithm.HS256) .compact(); } public String extractUserName(String token) { return extractClaim(token, Claims::getSubject); } public boolean isTokenValid(String token, UserDetails userDetails) { //System.out.println("JWTServiceImpl.isTokenValid()" + token); final String userName = extractUserName(token); return userName.equals(userDetails.getUsername()) && !isTokenExpired(token); } private boolean isTokenExpired(String token) { // TODO Auto-generated method stub return extractClaim(token, Claims::getExpiration).before(new Date()); } private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) { final Claims claims = extractAllClaims(token); return claimsResolver.apply(claims); } private Claims extractAllClaims(String token) { //System.out.println("JWTServiceImpl.extractAllClaims()"+token); return Jwts.parserBuilder().setSigningKey(getSignKey()).build().parseClaimsJws(token).getBody(); } private Key getSignKey() { //System.out.println("JWTServiceImpl.getSignKey() "+ secretKey); byte[] key = Decoders.BASE64.decode(secretKey); return Keys.hmacShaKeyFor(key); } }
what are claims ?
Claims are pieces of information embedded in a JSON Web Token (JWT) that convey specific details about the user or the context of the token.
Below are some predefined registered claims:
iss (Issuer): Identifies the principal that issued the JWT.
sub (Subject): Identifies the subject of the JWT, typically the user.
aud (Audience): Identifies the recipients that the JWT is intended for.
exp (Expiration Time): Indicates when the token will expire.
nbf (Not Before): Indicates the time before which the token must not be accepted for processing.
iat (Issued At): Indicates when the token was issued.
Create a JWT Filter
What is filter in spring boot?
In Spring boot a filter is a java class that intercepts and process HTTP requests and responses before they reach the controller or after they leave the controller.
Why JWT Filter ?
A JWT Filter is helpful to intercept HTTP requests to check for JSON Web Tokens (JWTs) used for authentication and authorization. It validates the presence and integrity of the token by checking its signature and expiration, ensuring that only requests with valid tokens can access protected resources. By centralizing this authentication logic, the JWTFilter enhances security, simplifies user permission management, and supports a stateless architecture, allowing the application to scale efficiently without relying on server-side session data.
How to implement JWT Filter ?
Extend org.springframework.web.filter.OncePerRequestFilter, which ensures that the filter is applied only once per request, preventing duplicate processing.
Override doFilterInternal
in doFilterInternal check if Bearer token exists in the request or not, if not then return.
if token exists, extract username from token and check if the username is not null and if there’s no existing authentication in the security context. This ensures that the filter processes the request only if the user is not already authenticated.
if user is not already authenticated (no existing authentication in the security context) then load the user details using the UserDetailsService based on the extracted username.
Then validate the token.
If the token is valid, a UsernamePasswordAuthenticationToken is created and set in the security context, allowing the application to recognize the user as authenticated.
Finally, the filterChain.doFilter(request, response) call is made to continue processing the request, passing control to the next filter or handler in the chain.
package com.hk.sec.prep.config;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import com.hk.sec.prep.services.JWTService;
import com.hk.sec.prep.services.UserService;
import io.micrometer.common.util.StringUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
@Component
@RequiredArgsConstructor
public class JWTAuthFilter extends OncePerRequestFilter {
@Autowired
private final JWTService jwtService;
@Autowired
private final UserService userService;
/**
* The main method that gets called for every request
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Get the Authorization header from the request
final String authHeader = request.getHeader("Authorization");
// If the Authorization header is missing or doesn't start with "Bearer ", proceed with the next filter
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
try {
// Extract the JWT token from the Authorization header
final String jwt = authHeader.substring(7);
// Extract the username (or email) from the JWT token
final String userEmail = jwtService.extractUserName(jwt);
// If the username is not empty and there is no current authentication in the security context
if (StringUtils.isNotEmpty(userEmail) && SecurityContextHolder.getContext().getAuthentication() == null) {
// Load user details using the username
UserDetails userDetails = userService.userDetailsService().loadUserByUsername(userEmail);
// Validate the JWT token
if (jwtService.isTokenValid(jwt, userDetails)) {
// Create an empty SecurityContext
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
// Create a UsernamePasswordAuthenticationToken with user details and authorities
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
// Set the details of the authentication token with the request details
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// Set the authentication in the security context
securityContext.setAuthentication(usernamePasswordAuthenticationToken);
SecurityContextHolder.setContext(securityContext);
}
}
} catch (Exception e) {
// Print the stack trace if there is an exception during the token validation process
e.printStackTrace();
}
// Proceed with the next filter in the filter chain
filterChain.doFilter(request, response);
}
}
- Create a Spring security configuration class as below.
package com.hk.sec.prep.config;
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.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.annotation.web.configurers.AbstractHttpConfigurer;
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;
import com.hk.sec.prep.entity.Role;
import com.hk.sec.prep.services.UserService;
import lombok.RequiredArgsConstructor;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SpringSecurityConfiguration {
@Autowired
private final JWTAuthFilter jwtAuthFilter;
@Autowired
private final UserService userService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(request -> request
.requestMatchers("/public/**").permitAll()
.requestMatchers("/private/admin/**").hasAuthority(Role.ADMIN.name())
.requestMatchers("/private/user/**").hasAnyAuthority(Role.USER.name(),Role.ADMIN.name())
.anyRequest().authenticated())
.sessionManagement(manager -> manager.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userService.userDetailsService());
daoAuthenticationProvider.setPasswordEncoder(PasswordEncoder());
return daoAuthenticationProvider;
}
@Bean
public PasswordEncoder PasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
Create your controllers to login and authentication and authorization. refer below code for better understanding.
package com.hk.sec.prep.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.hk.sec.prep.dto.LogInResponse; import com.hk.sec.prep.dto.LoginRequest; import com.hk.sec.prep.dto.RefreshTokenRequest; import com.hk.sec.prep.dto.SignUpRequest; import com.hk.sec.prep.entity.User; import com.hk.sec.prep.services.AuthenticationService; @RestController @RequestMapping("/public/auth") @CrossOrigin(origins = "http://localhost:4200") public class AuthController { @Autowired private AuthenticationService authenticationService; @PostMapping("/signUp") public ResponseEntity<User> signUp(@RequestBody SignUpRequest signUpRequest) { return ResponseEntity.ok(authenticationService.signUp(signUpRequest)); } @PostMapping("/logIn") public ResponseEntity<LogInResponse> signUp(@RequestBody LoginRequest loginRequest) { return ResponseEntity.ok(authenticationService.logIn(loginRequest)); } @PostMapping("/refreshToken") public ResponseEntity<LogInResponse> refreshToken(@RequestBody RefreshTokenRequest refreshTokenRequest) { return ResponseEntity.ok(authenticationService.refreshToken(refreshTokenRequest)); } }
The Complete code: https://github.com/hemantjava96/springSecurity-JWT-boot3.x