JWT TOKEN FOR API AUTHENTICATION AND AUTHORIZATION
Haroon Idrees
Senior Softwares Lead/Architect/Engineer| Java | Spring-Boot | Microservices | Cloud | Openshift | AWS | Azure | GCP| Angular | Oracle | Fullstack
In the cloud-native web application development, where scalability, statelessness, and distributed architectures are key considerations, robust authentication and authorization mechanisms are vital for safeguarding user data's security and integrity. Traditional session-based authentication methods fall short in meeting these requirements. However, the combination of JSON Web Tokens (JWT) and Spring Security emerges as a powerful solution to address these challenges effectively.
JSON Web Token (JWT) has a basic structure consisting of three parts: the header, payload, and signature.
The header contains information about the token, such as the type (JWT) and the signing algorithm used.
The payload contains claims, which are statements about the user and additional data. Claims can be registered, public, or private, and they provide information about the user's identity and other relevant details.
The signature is created by taking the encoded header, encoded payload, and a secret or private key. It ensures the integrity of the token and verifies that it has not been tampered with during transmission. The server can validate the signature using the secret or public key.
When combined, these three parts form a JWT. It can be used for authentication and authorization purposes in stateless architectures, where the server does not need to store session information and can verify the token with each request.
Today's article is a continuation of our last article Custom Form-based Implementation with Database Integration. We will utilize the previously committed code and incorporate the JWT component into it for authenticating the RESTful API.
Before delving into the code, let's first examine the flow of JWT authentication.
Here is a description of the JWT ID token flow for Client, outlined in bullet points:
1. The client sends the user's credentials to the authentication server (e.g., via an HTTP POST request) to request authentication.
2. The authentication server verifies the credentials and, if valid, generates a JSON Web Token (JWT) ID token. The authentication server includes necessary user information (such as user ID, roles, or claims) in the JWT payload.
3. The authentication server signs the JWT with a secret key, ensuring its authenticity and integrity. The authentication server sends the signed JWT ID token back to the client as a response to the authentication request.
4. The client receives the JWT ID token and stores it securely (e.g., in browser local storage or a cookie or in memory) for future use.
5. Subsequent API requests from the client include the JWT ID token as an authorization header (e.g., "Authorization: Bearer <token>") to prove the user's identity and access rights.
6. The server-side API endpoint that receives the request validates the JWT signature, ensuring its authenticity.
7. If the JWT is valid and has not expired, the server-side API allows the requested operation or resource access. Otherwise, it returns an appropriate error response.
Now, it's time to checkout the code from our previous post from following branch and begin the process of implementing JWT authentication.
First, we need to update pom.xml file, add the jwt dependencies and remove thymeleaf dependencies.
<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>
<!-- We also added spring-openapi-doc dependency for testing purpose. -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.0.0</version>
</dependency>
Now we need to add following properties in application.properties
#JWT specific self-explanatory keys
# generated key through java program written in SignatureKey.java repo to sign toke
application.security.jwt.secret-key= WGAC++P2jRIROphWc0837vsykrO6J0AzGQik4Kr2HTU=
# expiration time in millis
application.security.jwt.expiration= 86400000
# spring doc url for testing
springdoc.swagger-ui.path=/swagger-ui.html
Next, it is necessary to develop a JWT token utility service that will be responsible for generating & verifying tokens.
This utility class will be used by authenticate endpoint and filter which intercept resources endpoints to validate token.
@Servic
public class JwtTokenUtilService {
@Value("${application.security.jwt.secret-key}")
private String secretKey;
@Value("${application.security.jwt.expiration}")
private long jwtExpiration;
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
public String generateToken(UserDetails userDetails) {
return generateToken(getStringObjectMap(userDetails), userDetails);
}
private static Map<String, Object> getStringObjectMap(UserDetails userDetails) {
Map<String,Object> claims = new HashMap<>();
claims.put("roles", userDetails.getAuthorities().stream().map(simpelAuthority -> simpelAuthority.getAuthority()).collect(Collectors.toList()));
return claims;
}
public String generateToken(
Map<String, Object> extraClaims,
UserDetails userDetails
) {
return buildToken(extraClaims, userDetails, jwtExpiration);
}
private String buildToken(
Map<String, Object> extraClaims,
UserDetails userDetails,
long expiration) {
return Jwts
.builder()
.setClaims(extraClaims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSignInKey(), SignatureAlgorithm.HS256)
.compact();
}
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
private Claims extractAllClaims(String token) {
return Jwts
.parserBuilder()
.setSigningKey(getSignInKey())
.build()
.parseClaimsJws(token)
.getBody();
}
private Key getSignInKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
}{
The provided code snippet shows the implementation of the JwtTokenUtilService class, which is responsible for generating and validating JWT tokens in a Spring Boot application.
Let's examine the important lines of code in this class:
11-14. The extractUsername method extracts the username from a given JWT token by utilizing the Claims class and its getSubject method.
16-20. The extractClaim method is a generic function that can extract any claim from a JWT token. It takes a Function parameter, claimsResolver, which determines the type of claim to be extracted.
23-27. The generateToken method generates a JWT token based on the provided UserDetails object. It uses the getStringObjectMap method to retrieve the user's roles and add them as claims to the token.
30-33. The getStringObjectMap method converts the user's authorities (roles) into a list and adds them to a claims map.
36-42. The overloaded generateToken method allows the addition of extra claims to the JWT token, in addition to the user details. It calls the buildToken method to construct the token.
45-61. The buildToken method constructs the JWT token by setting the claims, subject, issued-at time, expiration time, and signing the token using the secret key obtained from the getSignInKey method.
64-72. The isTokenValid method checks the validity of a JWT token. It extracts the username from the token, compares it with the username from the UserDetails object, and verifies that the token has not expired.
领英推荐
75-79. The isTokenExpired method checks whether a given token has expired by comparing its expiration time with the current time.
82-86. The extractExpiration method extracts the expiration date from a JWT token.
89-94. The extractAllClaims method parses and extracts all the claims from a JWT token. It uses the parseClaimsJws method of the Jwts class and the getBody method to retrieve the claims.
97-102. The getSignInKey method decodes the secret key and returns a Key object that is used for signing and verifying the JWT token. It uses the hmacShaKeyFor method from the Keys class.
These lines of code collectively provide the necessary functionality for generating and validating JWT tokens in the application.
In second part, we need to write interceptor to validate token before accessing our application endpoints.
@Componen
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenUtilService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
if (request.getServletPath().contains("/api/v1/auth")) {
filterChain.doFilter(request, response);
return;
}
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String userEmail;
if (authHeader == null ||!authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
jwt = authHeader.substring(7);
userEmail = jwtService.extractUsername(jwt);
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
if (jwtService.isTokenValid(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails.getUsername(),
null,
userDetails.getAuthorities()
);
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
The provided code snippet shows the implementation of the JwtAuthenticationFilter class, which is a Spring Security filter responsible for handling JWT authentication.
Let's examine the important lines of code in this class:
15-20. The class extends the OncePerRequestFilter class, which ensures that the doFilterInternal method is executed only once per request.
22-23. The JwtTokenUtilService and UserDetailsService are injected as dependencies using constructor injection.
26-40. The doFilterInternal method is the main logic for processing the authentication. It is responsible for extracting the JWT token from the request header, validating it, and setting the authenticated user in the SecurityContextHolder.
27-30. The method checks if the request path contains "/api/v1/auth". If it does, the filter chain is continued without any authentication checks. This allows the authentication API endpoint to be accessible without authentication.
32-37. The method retrieves the JWT token from the "Authorization" header and checks if it starts with the "Bearer " prefix. If not, the filter chain is continued without any authentication checks.
42-53. If the user email is not null and there is no existing authentication in the SecurityContextHolder, the loadUserByUsername method of the UserDetailsService is called to retrieve the user details based on the email.
55-59. The isTokenValid method of the JwtTokenUtilService is called to check if the JWT token is valid. If valid, an UsernamePasswordAuthenticationToken is created with the user's email, authorities, and set as the authenticated token in the SecurityContextHolder.
Finally, the filter chain is continued to allow the request to proceed.
The JwtAuthenticationFilter plays a crucial role in the authentication process by intercepting incoming requests, extracting the JWT token, validating it, and setting the authenticated user in the security context.
In last part we also need to update Security Configuration as following
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.authorizeHttpRequests()
.requestMatchers("/api/v1/auth/**","/v3/api-docs/**","/swagger-ui.html","/swagger-ui/**")
.permitAll()
.requestMatchers("/api/v1/admin/**").hasAuthority("Admin")
.anyRequest()
.authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
The provided code snippet shows the configuration of the security filter chain. This code snippet configures the security filter chain to disable CSRF protection, define authorization rules for different request patterns, set the session creation policy to stateless, configure an authentication provider, and add a custom JWT authentication filter to the filter chain.
We have completed all the necessary changes, and now it is time to proceed with testing.
You can find complete source from following repo
We need to run to SpringSecurityApplication class and browse to https://localhost:8080/swagger-ui.html. In SpringSecurityApplication class we created two sample users, one with no role and other with admin role. we will use one of the user's credentials to call authenticate endpoint.
we call authenticate controller and receive above jwt token in response. we can verify and analyze token from https://jwt.io/ website.
Now, we will use the above token as a header to call welcome controller endpoint.
Hurry! we are done, we successfully manage to access welcome controller with above JWT token.
Conclusion
This article focused on implementing JWT authentication using Spring Security with a database for managing users and roles. It provided a step-by-step guide for incorporating JWT authentication into a RESTful API, explaining the flow of JWT authentication and key components such as the JWT token utility service and the JWT authentication filter. The article emphasized the importance of robust authentication and authorization mechanisms in cloud-native web applications and highlighted the advantages of using JWT and Spring Security for addressing security and scalability concerns. By following the guide, developers can enhance the security and integrity of their applications by implementing JWT authentication effectively.
In the upcoming article, we will explore the option of storing the JWT token in an HTTP-only cookie instead of sending it with each API call. Stay tuned for more information.