In the previous article, we have secured the REST API with Spring Security Basic Authentication. Now we are gonna add JWT Authentication and Role-Based Authorization to the same REST API that we have implemented previously using Spring Security 5.
Note: This article is about implementing JWT authentication by customizing the spring security which is outdated and not recommended. Hence, it is recommended to Secure Spring REST API with OAuth2 JWT Authentication
Introduction to JWT
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.
The official JWT website can be found here: https://jwt.io/
Let’s Get Started
We will be implementing JWT authentication with Spring Security for performing 2 operations:
- Generating JWT – Expose a POST API with mapping /authenticate. On passing correct username and password it will generate a JSON Web Token (JWT)
- Validating JWT – If user tries to access Product API with mapping /product/**, it will allow access only if request has a valid JSON Web Token (JWT) and users with admin role only will be allowed to perform update/delete operations.
Step 1: Add JWT dependency
pom.xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
Step 2: Implement JWT Token Utility
JwtTokenUtil.java
The JwtTokenUtil
is responsible for performing JWT operations like creation and validation of the token. It makes use of the io.jsonwebtoken.Jwts
for achieving this.
package com.javachinna.util;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
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.Component;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
@Component
public class JwtTokenUtil implements Serializable {
private static final long serialVersionUID = -2550185165626007488L;
public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60;
@Value("${jwt.secret}")
private String secret;
// retrieve username from jwt token
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
// retrieve expiration date from jwt token
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
// for retrieveing any information from token we will need the secret key
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
// check if the token has expired
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
// generate token for user
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return doGenerateToken(claims, userDetails.getUsername());
}
// while creating the token -
// 1. Define claims of the token, like Issuer, Expiration, Subject, and the ID
// 2. Sign the JWT using the HS512 algorithm and secret key.
// 3. According to JWS Compact
// Serialization(https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-3.1)
// compaction of the JWT to a URL-safe string
private String doGenerateToken(Map<String, Object> claims, String subject) {
return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000)).signWith(SignatureAlgorithm.HS512, secret).compact();
}
// validate token
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
Step 3: Implement JWT Request Filter
JwtRequestFilter.java
The JwtRequestFilter
extends the Spring Web Filter OncePerRequestFilter
class. For any incoming request, this Filter class gets executed. It checks if the request has a valid JWT token. If it has a valid JWT Token then it sets the Authentication in the context, to specify that the current user is authenticated.
package com.javachinna.config;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.context.annotation.Profile;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
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.javachinna.service.UserDetailsServiceImpl;
import com.javachinna.util.JwtTokenUtil;
import io.jsonwebtoken.ExpiredJwtException;
import lombok.RequiredArgsConstructor;
@Component
@Profile(Profiles.JWT_AUTH)
@RequiredArgsConstructor
public class JwtRequestFilter extends OncePerRequestFilter {
private final UserDetailsServiceImpl jwtUserDetailsService;
private final JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
final String requestTokenHeader = request.getHeader("Authorization");
String username = null;
String jwtToken = null;
// JWT Token is in the form "Bearer token". Remove Bearer word and get
// only the Token
if (requestTokenHeader != null) {
if (requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
} catch (IllegalArgumentException e) {
System.out.println("Unable to get JWT Token");
} catch (ExpiredJwtException e) {
System.out.println("JWT Token has expired");
}
} else {
logger.warn("JWT Token does not begin with Bearer String");
}
}
// Once we get the token validate it.
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.jwtUserDetailsService.loadUserByUsername(username);
// if token is valid configure Spring Security to manually set
// authentication
if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// After setting the Authentication in the context, we specify
// that the current user is authenticated. So it passes the
// Spring Security Configurations successfully.
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}
Step 4: Implement JWT Authentication Entry point
JwtAuthenticationEntryPoint.java
JwtAuthenticationEntryPoint
extends Spring’s AuthenticationEntryPoint
class and overrides its method commence. It rejects every unauthenticated request and sends error code 401.
package com.javachinna.config;
import java.io.IOException;
import java.io.Serializable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = -7858869558953243875L;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
Step 5: Configure Spring Security JWT Authentication
Profiles.java
Create a constant for JWT Authentication profile
package com.javachinna.config;
public class Profiles {
private Profiles() {
}
public static final String BASIC_AUTH = "basicauth";
public static final String JWT_AUTH = "jwtauth";
}
JwtAuthSecurityConfig.java
JwtAuthSecurityConfig
class extends the WebSecurityConfigurerAdapter
which is a convenience class that allows customization to both WebSecurity and HttpSecurity.
@Profile(Profiles.JWT_AUTH)
annotation is used to enable JWT authentication only when the application is run with “jwtauth
” profile.
package com.javachinna.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.javachinna.model.Role;
import lombok.RequiredArgsConstructor;
@Profile(Profiles.JWT_AUTH)
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class JwtAuthSecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final UserDetailsService jwtUserDetailsService;
private final JwtRequestFilter jwtRequestFilter;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// configure AuthenticationManager so that it knows from where to load
// user for matching credentials
// Use BCryptPasswordEncoder
auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// Disable CSRF
httpSecurity.csrf().disable()
// Only admin can perform HTTP delete operation
.authorizeRequests().antMatchers(HttpMethod.DELETE).hasRole(Role.ADMIN)
// any authenticated user can perform all other operations
.antMatchers("/products/**").hasAnyRole(Role.ADMIN, Role.USER)
// Permit all other request without authentication
.and().authorizeRequests().anyRequest().permitAll()
// Reject every unauthenticated request and send error code 401.
.and().exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
// We don't need sessions to be created.
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// Add a filter to validate the tokens with every request
httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
}
Step 6: Configure Swagger with JWT Authentication
SwaggerConfig.java
In order to enable JWT Authentication in Swagger-UI, we need to add Bearer Authorization Security Scheme and Security Context to the Swagger Configuration as highlighted below
package com.javachinna.config;
import java.util.Collections;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ApiKey;
import springfox.documentation.service.AuthorizationScope;
import springfox.documentation.service.BasicAuth;
import springfox.documentation.service.Contact;
import springfox.documentation.service.SecurityReference;
import springfox.documentation.service.SecurityScheme;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
public class SwaggerConfig {
private static final String BASIC_AUTH = "basicAuth";
private static final String BEARER_AUTH = "Bearer";
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2).select().apis(RequestHandlerSelectors.basePackage("com.javachinna")).paths(PathSelectors.any()).build().apiInfo(apiInfo())
.securitySchemes(securitySchemes()).securityContexts(List.of(securityContext()));
}
private ApiInfo apiInfo() {
return new ApiInfo("Product REST API", "Product API to perform CRUD opertations", "1.0", "Terms of service",
new Contact("Java Chinna", "www.javachinna.com", "[email protected]"), "License of API", "API license URL", Collections.emptyList());
}
private List<SecurityScheme> securitySchemes() {
return List.of(new BasicAuth(BASIC_AUTH), new ApiKey(BEARER_AUTH, "Authorization", "header"));
}
private SecurityContext securityContext() {
return SecurityContext.builder().securityReferences(List.of(basicAuthReference(), bearerAuthReference())).forPaths(PathSelectors.ant("/products/**")).build();
}
private SecurityReference basicAuthReference() {
return new SecurityReference(BASIC_AUTH, new AuthorizationScope[0]);
}
private SecurityReference bearerAuthReference() {
return new SecurityReference(BEARER_AUTH, new AuthorizationScope[0]);
}
}
Step 7: Create JWT Request & Response Classes
JwtRequest.java
This model class is used for storing the username and password received from the client.
package com.javachinna.model;
import java.io.Serializable;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class JwtRequest implements Serializable {
private static final long serialVersionUID = 5926468583005150707L;
@ApiModelProperty(example = "user")
private String username;
@ApiModelProperty(example = "password")
private String password;
}
JwtResponse.java
This model class is used for creating a response containing the JWT to be returned to the user.
package com.javachinna.model;
import java.io.Serializable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class JwtResponse implements Serializable {
private static final long serialVersionUID = -8091879091924046844L;
private final String jwttoken;
}
Step 8: Implment JWT Authentication Controller
JwtAuthenticationController.java
Expose a POST API /authenticate
using the JwtAuthenticationController
. The POST API gets the username and password in the body. Using Spring Authentication Manager we authenticate the username and password. If the credentials are valid, a JWT token is created using the JWTTokenUtil
and provided to the client.
package com.javachinna.controller;
import org.springframework.context.annotation.Profile;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
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.RestController;
import com.javachinna.config.Profiles;
import com.javachinna.model.JwtRequest;
import com.javachinna.model.JwtResponse;
import com.javachinna.util.JwtTokenUtil;
import lombok.RequiredArgsConstructor;
@Profile(Profiles.JWT_AUTH)
@RestController
@CrossOrigin
@RequiredArgsConstructor
public class JwtAuthenticationController {
private final AuthenticationManager authenticationManager;
private final JwtTokenUtil jwtTokenUtil;
@PostMapping("/authenticate")
public ResponseEntity<?> createAuthenticationToken(@RequestBody JwtRequest authenticationRequest) throws Exception {
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword()));
final UserDetails userDetails = (UserDetails) authentication.getPrincipal();
final String token = jwtTokenUtil.generateToken(userDetails);
return ResponseEntity.ok(new JwtResponse(token));
}
}
Step 9: Configure JWT properties
application.properties
The secret key is used to create a unique hash and for retrieving any information from the token we will need the secret key
jwt.secret=javachinna
Run with JwtAuth Profile
You can run the application using mvn spring-boot:run -Dspring-boot.run.profiles=jwtauth
and hit the url http://localhost:8080/swagger-ui.html in the browser. The swagger UI will list jwt-authentication-controller
and product-controller
as shown below
Obtain JWT Token
We can get JWT token by executing the /authenticate
endpoint with user credentials in the request body
JWT Request
JWT Response
The token in the above response should be used in the Authorization request header to call any other API
Authorize API
Click on the Authorize button in Swagger UI as shown below
Enter “Bearer
” with JWT token in the Value field as shown below and click on Authorize button.
Test the Services
Create Product
Click on the Create Product endpoint and click on “Try it out” and then execute
Request
Response
Delete Product
Request
Response
Since the product can be deleted by users with the “Admin” role only, we are getting HTTP Status 403 Forbidden in the response
Obtain the JWT token with the admin credentials and enter the token in the Authorize dialog and then execute the delete API again. The product will be deleted.
Method Level Role Based Authorization
We have already added the method level authorization in the previous article. So, we are just gonna test it now.
Update Product
Click on the Create Product endpoint and click on “Try it out” and then execute
Request
Response
Since the product can be updated by users with the “Admin” role only, we are getting HTTP Status 403 Forbidden in the response
JWT Authentication Logout
You can log out the currently authorized user in the Swagger-UI Authorize dialog as shown below and authorize with a JWT token of another user.
Test Services with Postman
You can also test the services in Postman by providing a JWT token in the Authorization request header as shown below
Decode JWT Token
You can decode the JWT token here with the JWT secret and see token Header, Payload, and Signature details as shown below
Source Code
As always, you can get the source code from the Github below
https://github.com/JavaChinna/spring-boot-rest-jwt-auth
Conclusion
That’s all folks! In this article, you’ve learned how to implement JWT authentication and authorization for Spring Boot RESTful services.
Hey,
Can you explain to me why here you got user from DB, and not doing the same as you did in the previous post ?
Thanks
@PostMapping(“/authenticate”)
public ResponseEntity createAuthenticationToken(@RequestBody JwtRequest authenticationRequest) throws Exception {
authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
final String token = jwtTokenUtil.generateToken(userDetails);
return ResponseEntity.ok(new JwtResponse(token));
}
like this..
public ResponseEntity authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = tokenProvider.createToken(authentication);
LocalUser localUser = (LocalUser) authentication.getPrincipal();
return ResponseEntity.ok(new JwtAuthenticationResponse(jwt, GeneralUtils.buildUserInfo(localUser)));
}
there is any difference ?
Thanks for pointing that out. Now I have removed the unnecessary DB call to fetch user details.
How to get the user details if we want to register whare we need to registet
You can refer to the AuthController here which has user registration API as well. Please note that the JWT authentication implemented in these projects is sort of custom security and a bit outdated. Spring Security itself provides support for JWT authentication. So I’m in the process of writing an article for OAuth 2.0 token-based authentication with Spring Security. Let me know if you are interested in that.
Here is the latest article for OAuth2 JWT authentication: https://www.javachinna.com/secure-spring-rest-api-oauth2-jwt-authentication/
How to do role based authentication.
Can you implement that over the function that code I copied it was not working.
This post is about implementing role-based authorization and it should work fine as it is. I’m not sure why it is not working for you. I would recommend following the latest post for OAuth2 JWT authentication: https://www.javachinna.com/secure-spring-rest-api-oauth2-jwt-authentication/