How to Implement JWT Token Based Authentication in Spring Boot Microservices
JWT (JSON Web Token) tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties. It is highly recommended and used authentication method for REST API & Webservices.
Today we are going to see how to implement JWT token based authentication in spring boot microservices to securely communicate and transfer the data’s between the client(any client applications, angular/react/vue in modern application world) and server side applications.
Though process for Token Based Authentication
- Spring security is needed for JWT token based authentication
- Ensure the signup / login REST API accesses are allowed in spring security (or disable these two API’s)
- During signup, save the captured user details in the user table then generate and respond the JWT token to client side applications.
- During login, validate the user details then generate and respond the JWT token to client side applications, if the user credentials are correct.
- Ensure you have kept the same secret keys in all the microservices, to allow the validation across the microservices are allowed, even though the key is generated from different microservices.
- All the REST API calls are allowed to access only when the JWT token is validated.
Add the JWT dependency in pom.xml
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
JwtAuthenticationEntryPoint.java:
package com.ngdeveloper.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 = 1L; @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); } }
JwtAuthenticationFilter.java
package com.ngdeveloper.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.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.web.filter.OncePerRequestFilter; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.SignatureException; public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private UserDetailsService userDetailsService; @Autowired private TokenProvider jwtTokenUtil; @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { String header = req.getHeader(JwtConstants.HEADER_STRING); String username = null; String authToken = null; if ("OPTIONS".equalsIgnoreCase(req.getMethod())) { res.setStatus(HttpServletResponse.SC_OK); chain.doFilter(req, res); } else { if (header != null && header.startsWith(JwtConstants.TOKEN_PREFIX)) { authToken = header.replace(JwtConstants.TOKEN_PREFIX, ""); try { username = jwtTokenUtil.getUsernameFromToken(authToken); } catch (IllegalArgumentException e) { logger.error("an error occured during getting username from token", e); } catch (ExpiredJwtException e) { logger.warn("the token is expired and not valid anymore", e); } catch (SignatureException e) { logger.error("Authentication Failed. Username or Password not valid."); } } else { //logger.warn("couldn't find bearer string, will ignore the header"); } if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authentication = jwtTokenUtil.getAuthentication(authToken, SecurityContextHolder.getContext().getAuthentication(), userDetails); // UsernamePasswordAuthenticationToken authentication = new // UsernamePasswordAuthenticationToken(userDetails, null, Arrays.asList(new // SimpleGrantedAuthority("ROLE_ADMIN"))); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(req)); logger.info("authenticated user " + username + ", setting security context"); SecurityContextHolder.getContext().setAuthentication(authentication); } } chain.doFilter(req, res); } } }
TokenProvider.java
package com.ngdeveloper.config; import java.io.Serializable; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.function.Function; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; @Component public class TokenProvider implements Serializable { private static final long serialVersionUID = -4513315178320475145L; private static final Logger LOGGER = LoggerFactory.getLogger(TokenProvider.class); public String getUsernameFromToken(String token) { return getClaimFromToken(token, Claims::getSubject); } 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); } private Claims getAllClaimsFromToken(String token) { return Jwts.parser().setSigningKey(JwtConstants.SIGNING_KEY).parseClaimsJws(token).getBody(); } private Boolean isTokenExpired(String token) { final Date expiration = getExpirationDateFromToken(token); return expiration.before(new Date()); } /* by default 1 hour */ public String generateToken(Authentication authentication) { final String authorities = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority) .collect(Collectors.joining(",")); return Jwts.builder().setSubject(authentication.getName()).claim(JwtConstants.AUTHORITIES_KEY, authorities) .signWith(SignatureAlgorithm.HS256, JwtConstants.SIGNING_KEY) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + JwtConstants.ACCESS_TOKEN_VALIDITY_SECONDS * 1000)) .compact(); } /* 1 week token expiry setting*/ // 604800000 = 168*3600000 // 3600000 - 1 hour in milliseconds // 168 - 7* 24 ( 7 week per day * 24 hours per day) public String generateTokenForAWeekExpiry(Authentication authentication) { final String authorities = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority) .collect(Collectors.joining(",")); return Jwts.builder().setSubject(authentication.getName()).claim(JwtConstants.AUTHORITIES_KEY, authorities) .signWith(SignatureAlgorithm.HS256, JwtConstants.SIGNING_KEY) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + 604800000)) .compact(); } public Boolean validateToken(String token, UserDetails userDetails) { final String username = getUsernameFromToken(token); return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); } UsernamePasswordAuthenticationToken getAuthentication(final String token, final Authentication existingAuth, final UserDetails userDetails) { UsernamePasswordAuthenticationToken temp=null; try { final JwtParser jwtParser = Jwts.parser().setSigningKey(JwtConstants.SIGNING_KEY); final Jws<Claims> claimsJws = jwtParser.parseClaimsJws(token); final Claims claims = claimsJws.getBody(); final Collection<? extends GrantedAuthority> authorities = Arrays .stream(claims.get(JwtConstants.AUTHORITIES_KEY).toString().split(",")).map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); temp=new UsernamePasswordAuthenticationToken(userDetails, "", authorities); }catch(Exception e) { e.printStackTrace(); LOGGER.error(e.getMessage()); } return temp; } }
WebSecurityConfig.java
package com.ngdeveloper.config; import javax.annotation.Resource; 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.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; 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.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import com.google.common.collect.ImmutableList; @Configuration @EnableWebSecurity // @EnableGlobalMethodSecurity(securedEnabled = true) // for https @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean public CorsConfigurationSource corsConfigurationSource() { final CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(ImmutableList.of("*")); configuration.setAllowedMethods(ImmutableList.of("HEAD", "GET", "POST", "PUT", "DELETE", "PATCH")); // setAllowCredentials(true) is important, otherwise: // The value of the 'Access-Control-Allow-Origin' header in the response must // not be the wildcard '*' when the request's credentials mode is 'include'. configuration.setAllowCredentials(true); // setAllowedHeaders is important! Without it, OPTIONS preflight request // will fail with 403 Invalid CORS request configuration.setAllowedHeaders(ImmutableList.of("Authorization", "Cache-Control", "Content-Type")); final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } @Resource(name = "userService") private UserDetailsService userDetailsService; @Autowired private JwtAuthenticationEntryPoint unauthorizedHandler; @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Autowired public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(encoder()); } /*@Bean CORSFilter corsFilter() { CORSFilter filter = new CORSFilter(); return filter; }*/ @Bean public JwtAuthenticationFilter authenticationTokenFilterBean() throws Exception { return new JwtAuthenticationFilter(); } @Override protected void configure(HttpSecurity http) throws Exception { http.cors().and().csrf().disable().authorizeRequests() .antMatchers("/token/*", "/user/register", "/user/login") .permitAll().anyRequest().authenticated().and().exceptionHandling() .authenticationEntryPoint(unauthorizedHandler).and().sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/actuator/**", "/v2/api-docs", "/webjars/**", "/swagger-resources/**", "/swagger-ui.html"); } @Bean public BCryptPasswordEncoder encoder() { return new BCryptPasswordEncoder(); } }
UserController.java [ This is the place we create token for successful register and login API Calls ]
package com.ngdeveloper.controller; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import javax.servlet.ServletException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; 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.RequestMethod; import org.springframework.web.bind.annotation.RestController; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; @RestController @RequestMapping("/user") public class UserController { private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class); @Autowired private UserDetailsService userDetailsService; @Autowired private com.ngdeveloper.config.TokenProvider jwtTokenUtil; @Autowired private AuthenticationManager authenticationManager; @RequestMapping(value = "/login", method = RequestMethod.POST) public String login(@RequestBody Map<String, String> json) throws ServletException { if (json.get("username") == null || json.get("password") == null) { throw new ServletException("Please fill in username and password"); } String userName = json.get("username"); String password = json.get("password"); String rememberMe = "N"; if(json.containsKey("rememberMe")) { // rememeberMe key will give the value like Y / N based on the selection from the admin panel login page rememberMe = json.get("rememberMe"); } User user = userService.findByUserName(userName); if (user == null) { LOGGER.info("user name not found" + userName); throw new ServletException("User name not found."); } else if (user.getUserRole().equalsIgnoreCase("SUBSCRIBER")) { LOGGER.info("Subscriber not allowed to login into dashboard!" + userName); throw new ServletException("You are not allowed!"); } PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); if (!passwordEncoder.matches(password, user.getPassword())) { throw new ServletException("Invalid login. Please check your username and password"); } final Authentication authentication = authenticationManager .authenticate(new UsernamePasswordAuthenticationToken(userName, password)); SecurityContextHolder.getContext().setAuthentication(authentication); if(rememberMe.equalsIgnoreCase("Y")) { final String token = jwtTokenUtil.generateTokenForAWeekExpiry(authentication); return token; }else { final String token = jwtTokenUtil.generateToken(authentication); return token; } } @RequestMapping(value = "/register", method = RequestMethod.POST) public String registerUser(@RequestBody User user) { String tokenResp = ""; String passwordTemp = user.getPassword(); try { user.setPassword(new BCryptPasswordEncoder().encode(user.getPassword())); // save to your own tables here // userService.save(user); final Authentication authentication = authenticationManager .authenticate(new UsernamePasswordAuthenticationToken(user.getUserName(), passwordTemp)); SecurityContextHolder.getContext().setAuthentication(authentication); final String token = jwtTokenUtil.generateToken(authentication); tokenResp = token; } catch (Exception e) { LOGGER.error(e.getMessage()); tokenResp = ""; } return tokenResp; } }
JwtConstants.java
package com.ngdeveloper.config; public class JwtConstants { public static final long ACCESS_TOKEN_VALIDITY_SECONDS = 5*60*60; public static final String SIGNING_KEY = "YOUR_SECRET_KEY"; public static final String TOKEN_PREFIX = "Bearer "; public static final String HEADER_STRING = "Authorization"; public static final String AUTHORITIES_KEY = "ROLE_KEY"; }
When you want to configure for multiple microservices, do the same process and make sure you have the same SECRET_KEY configured in your JWTConstants.java, so that the Token generated by one microservice can be used to pass to other microservices.
BlogController.java
package com.ngdeveloper.controller; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.io.IOUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; 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.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartHttpServletRequest; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; @RestController @RequestMapping("/blog") @PreAuthorize("hasRole('ADMIN') or hasRole('EDITOR') or hasRole('AUTHOR') or hasRole('CONTRIBUTOR')") public class BlogController { @ApiOperation(value = "API to return list of posts") @ApiResponses(value = {@ApiResponse(code = 200, message = "Success", response = Post.class), @ApiResponse(code = 403, message = Constants.FORBIDDEN), @ApiResponse(code = 422, message = Constants.NOT_FOUND), @ApiResponse(code = 417, message = Constants.EXCEPTION_FAILED)}) @PreAuthorize("hasRole('ADMIN') or hasRole('EDITOR')") @GetMapping("/posts") public List<Post> getAllPosts() { return posts; } }
This controller will be allowed to access only when the JWT token is validated in the filter. Because other than /user/register and /user/login REST API’s all the other REST API’s go through spring security, that will internally pass the request to the filter, which expects the valid bearer token in the request and allows to access the controller endpoints only when the token is valid.
I have also used @PreAuthorize to control the method level access based on the user roles.
Your sample request should be like this,
Endpoint: http://localhost:8443/ngdeveloper/blogs/post
Header: Bearer <JWT_Token>