JSON Web Tokens With Spring Cloud Microservices
At Keyhole, we have published several blogs about Microservices. We’ve talked about architectural patterns used in a Microservices environment such as service discovery and circuit breaker. We’ve even posted blogs on platforms and tools, such as the recent blog on Service Fabric.
One important piece of the architecture that we have glossed over is the security around Microservices. Specifically, authentication and authorization patterns.
There are several options when considering auth in Microservices, but this blog will specifically focus on using JSON Web Tokens.
JSON Web Tokens
Essentially, A JSON Web Token (JWT) is a self-contained authentication token that can contain information such as a user identifier, roles and permissions of a user, and anything else you might want to store in it. It can be easily read and parsed by anyone and can verified as authentic with a secret key. For a brief introduction to JSON Web Tokens, check out this page.
One advantage of using JSON Web Tokens with Microservices is that we can set it up so that it already contains any authorities that the user has. This means that each service does not need to reach out to our authorization service in order to authorize the user.
Another advantage that JWTs have is that they are serializable and small enough to fit inside a request header.
How It Works
The workflow is fairly simple. The first request is a POST to an unprotected authentication endpoint with a username and password.
On successful authentication, the response contains a JWT. All further requests come with an HTTP header that contains this JWT token in the form of Authorization: xxxxx.yyyyy.zzzzz
.
Any service-to-service requests will pass this header along so that any of the services can apply authorization along the way.
Now, To The Code!
The first thing we need to do is figure out how to generate these JWTs. Luckily, we’re not the first to try this and there are several libraries to choose from.
I chose Java JWT. Here is my implementation:
public class JsonWebTokenUtility { private SignatureAlgorithm signatureAlgorithm; private Key secretKey; public JsonWebTokenUtility() { // THIS IS NOT A SECURE PRACTICE! // For simplicity, we are storing a static key here. // Ideally, in a microservices environment, this key would kept on a // config server. signatureAlgorithm = SignatureAlgorithm.HS512; String encodedKey = "L7A/6zARSkK1j7Vd5SDD9pSSqZlqF7mAhiOgRbgv9Smce6tf4cJnvKOjtKPxNNnWQj+2lQEScm3XIUjhW+YVZg=="; secretKey = deserializeKey(encodedKey); } public String createJsonWebToken(AuthTokenDetailsDTO authTokenDetailsDTO) { String token = Jwts.builder().setSubject(authTokenDetailsDTO.userId).claim("email", authTokenDetailsDTO.email) .claim("roles", authTokenDetailsDTO.roleNames).setExpiration(authTokenDetailsDTO.expirationDate) .signWith(getSignatureAlgorithm(), getSecretKey()).compact(); return token; } private Key deserializeKey(String encodedKey) { byte[] decodedKey = Base64.getDecoder().decode(encodedKey); Key key = new SecretKeySpec(decodedKey, getSignatureAlgorithm().getJcaName()); return key; } private Key getSecretKey() { return secretKey; } public SignatureAlgorithm getSignatureAlgorithm() { return signatureAlgorithm; } public AuthTokenDetailsDTO parseAndValidate(String token) { AuthTokenDetailsDTO authTokenDetailsDTO = null; try { Claims claims = Jwts.parser().setSigningKey(getSecretKey()).parseClaimsJws(token).getBody(); String userId = claims.getSubject(); String email = (String) claims.get("email"); List roleNames = (List) claims.get("roles"); Date expirationDate = claims.getExpiration(); authTokenDetailsDTO = new AuthTokenDetailsDTO(); authTokenDetailsDTO.userId = userId; authTokenDetailsDTO.email = email; authTokenDetailsDTO.roleNames = roleNames; authTokenDetailsDTO.expirationDate = expirationDate; } catch (JwtException ex) { System.out.println(ex); } return authTokenDetailsDTO; } private String serializeKey(Key key) { String encodedKey = Base64.getEncoder().encodeToString(key.getEncoded()); return encodedKey; } }
Now that we have this utility class, we need to set up Spring Security in each of our Microservices.
For this, we will need a custom authentication filter that will read the request header if it is present. There is an authentication filter in Spring that already does this called the RequestHeaderAuthenticationFilter
that we can extend.
public class JsonWebTokenAuthenticationFilter extends RequestHeaderAuthenticationFilter { public JsonWebTokenAuthenticationFilter() { // Don't throw exceptions if the header is missing this.setExceptionIfHeaderMissing(false); // This is the request header it will look for this.setPrincipalRequestHeader("Authorization"); } @Override @Autowired public void setAuthenticationManager(AuthenticationManager authenticationManager) { super.setAuthenticationManager(authenticationManager); } }
At this point, the header has been converted into a Spring Authentication object in the form of a PreAuthenticatedAuthenticationToken
.
We now need an authentication provider that will read this token, authenticate it, and convert it to our own custom Authentication object.
public class JsonWebTokenAuthenticationProvider implements AuthenticationProvider { private JsonWebTokenUtility tokenService = new JsonWebTokenUtility(); @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { Authentication authenticatedUser = null; // Only process the PreAuthenticatedAuthenticationToken if (authentication.getClass().isAssignableFrom(PreAuthenticatedAuthenticationToken.class) && authentication.getPrincipal() != null) { String tokenHeader = (String) authentication.getPrincipal(); UserDetails userDetails = parseToken(tokenHeader); if (userDetails != null) { authenticatedUser = new JsonWebTokenAuthentication(userDetails, tokenHeader); } } else { // It is already a JsonWebTokenAuthentication authenticatedUser = authentication; } return authenticatedUser; } private UserDetails parseToken(String tokenHeader) { UserDetails principal = null; AuthTokenDetailsDTO authTokenDetails = tokenService.parseAndValidate(tokenHeader); if (authTokenDetails != null) { List<GrantedAuthority> authorities = authTokenDetails.roleNames.stream() .map(roleName -> new SimpleGrantedAuthority(roleName)).collect(Collectors.toList()); principal = new User(authTokenDetails.email, "", authorities); } return principal; } @Override public boolean supports(Class<?> authentication) { return authentication.isAssignableFrom(PreAuthenticatedAuthenticationToken.class) || authentication.isAssignableFrom(JsonWebTokenAuthentication.class); } }
With these components in place, we now have standard Spring Security wired up to use JWTs. When making service-to-service calls, we will need to pass the JWT along.
I used a Feign client, passing the JWT as a parameter.
@FeignClient("user-management-service") public interface UserManagementServiceAPI { @RequestMapping(value = "/authenticate", method = RequestMethod.POST) AuthTokenDTO authenticateUser(@RequestBody AuthenticationDTO authenticationDTO); @RequestMapping(method = RequestMethod.POST, value = "/roles") RoleDTO createRole(@RequestHeader("Authorization") String authorizationToken, @RequestBody RoleDTO roleDTO); @RequestMapping(method = RequestMethod.POST, value = "/users") UserDTO createUser(@RequestHeader("Authorization") String authorizationToken, @RequestBody UserDTO userDTO); @RequestMapping(method = RequestMethod.DELETE, value = "/roles/{id}") void deleteRole(@RequestHeader("Authorization") String authorizationToken, @PathVariable("id") int id); @RequestMapping(method = RequestMethod.DELETE, value = "/users/{id}") void deleteUser(@RequestHeader("Authorization") String authorizationToken, @PathVariable("id") int id); @RequestMapping(method = RequestMethod.GET, value = "/roles") Collection<RoleDTO> findAllRoles(@RequestHeader("Authorization") String authorizationToken); @RequestMapping(method = RequestMethod.GET, value = "/users") Collection<UserDTO> findAllUsers(@RequestHeader("Authorization") String authorizationToken); @RequestMapping(method = RequestMethod.GET, value = "/roles/{id}", produces = "application/json", consumes = "application/json") RoleDTO findRoleById(@RequestHeader("Authorization") String authorizationToken, @PathVariable("id") int id); @RequestMapping(method = RequestMethod.GET, value = "/users/{id}", produces = "application/json", consumes = "application/json") UserDTO findUserById(@RequestHeader("Authorization") String authorizationToken, @PathVariable("id") int id); @RequestMapping(method = RequestMethod.GET, value = "/users/{id}/roles") Collection<RoleDTO> findUserRoles(@RequestHeader("Authorization") String authorizationToken, @PathVariable("id") int id); @RequestMapping(method = RequestMethod.PUT, value = "/roles/{id}") void updateRole(@RequestHeader("Authorization") String authorizationToken, @PathVariable("id") int id, @RequestBody RoleDTO roleDTO); @RequestMapping(method = RequestMethod.PUT, value = "/users/{id}") void updateUser(@RequestHeader("Authorization") String authorizationToken, @PathVariable("id") int id, @RequestBody UserDTO userDTO); }
In order to pass the JWT along, I just grabbed it from Spring Security in my controller like this:
private String getAuthorizationToken() { String token = null; Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.getClass().isAssignableFrom(JsonWebTokenAuthentication.class)) { JsonWebTokenAuthentication jwtAuthentication = (JsonWebTokenAuthentication) authentication; token = jwtAuthentication.getJsonWebToken(); } return token; }
As you can tell, JWTs fit nicely in a distributed Microservices environment and provide plenty of versatility. When designing the security architecture for your next Microservices project, consider JSON Web Tokens.
Reference: | JSON Web Tokens With Spring Cloud Microservices from our JCG partner Thomas Kendlall at the Keyhole Software blog. |