Most of us have worked with services where we have a separate authenticator module or an IDP service which we invoke as the first step in our web workflow. Only once the user is authenticated we call other business services.
This is a typical approach several architectures takes. This makes our workflows simpler as once a user is authenticated we simply assume that we can call all other services without any potential risks.
However, this approach fails as soon as we move to a more distributed architecture where a service can be invoked from multiple sources. Few of the consumers can be a web app, mobile application, script jobs, or partners consuming the services directly. In such systems, you can’t assume that all the service consumers are authenticating the clients properly. Also as a service provider, it leaves you at the mercy of your clients.
In addition to user authentication sometimes a service provider needs to make sure that the service is restricting users to do only what they are authorised to do. However, if your auth is happening at the consumer side you have no means to do this securely. In this case, the best thing a service can do is to take the user identity as an input parameter or a HTTP header and simply assume that the user details which are coming in the service request are correct. Service will then have to base all its checks on these details. However, in the HTTP world, these details can be effortlessly manipulated and pose a huge security risk.
So how can we control who can call our service(Authentication) and what all can they do with your service(Authorise)?
JWT – JSON Web Tokens can be an answer to this problem.
What is JWT?
JWT is a self-contained JSON object which is used to securely transmit information between parties. JWT is signed and therefore cannot be forged or tempered. Once this token is obtained, it can then be passed in each downstream service call as an HTTP header. The service can check for JWT validity and can also obtain information stored in the JWT.
JWT can store custom claims which help us in storing and retrieving state information. This also helps to make our calls stateless as now all the state information is received in each request and so the server is not required to maintain any state or session information between calls.
Lets see a working example on how JWT can help us in securing services.
DEMO
Again, the intention of the demo is just to show how we can use JWT token to secure service to service communication. There are many aspects and standards which we need to follow(like token expiration, secure token transmission etc.) but for the sake of simplicity I am focusing on the below points:
- Token Generation with state information
- Token Verification in service calls
There are several tools and products which we can use for generating these tokens. These specialized systems have several other features to make sure you are fully compliant with the protocol standards. However again for the sake of simplicity, we will be generating our own tokens without using any specialized product and will be using these tokens to do authentication and authorization in each service call.
In case you want to learn more about any such specialised product you can check Keycloak.
Major components of the demo are as follows
- Authentication Service – User credentials verification and token generation.
- Service1 – Level 1 service. Assume that this service is getting calls from some external client like UI or Postman.
- Service2 – This service is called by Service 1 internally. This service is secured using JWT and thus even if this service is getting called from multiple different sources we can rest assured that no unauthenticated user will be able to fetch any data.
GENERATING A JWT TOKEN
Authentication service can be called by any service or other clients to validate user credentials and generate a user token in case credentials are valid. Pay special attention to the claims which we are adding in the JWT. We can add our own custom claims which can be used across applications.
Alert! 🚨: The purpose of the below code is to just demo JWT generation. This in no way a production ready code and there are many things which need hardening here before we can securely issue token in any actual application. In ideal scenario we use certificates for signing JWT tokens.
@RestController
public class TokenGenerator {
@RequestMapping(value = "/auth", method = RequestMethod.POST)
public String getJWTToken(@RequestBody Credentials credentials) throws UnsupportedEncodingException {
try {
// Validate username and password
boolean isValid = validateCredentials(credentials.getUsername(), credentials.getPassword());
Algorithm algorithm = Algorithm.HMAC256("secret");
String token = JWT.create()
.withIssuer("pulgupta")
.withSubject(credentials.getUsername())
.withClaim("preferred_username", credentials.getUsername())
.sign(algorithm);
return token;
} catch (UnsupportedEncodingException exception){
throw exception;
} catch (JWTCreationException exception){
throw exception;
}
}
private boolean validateCredentials(String username, String password) {
return true;
}
}
We are here first checking if the user credentials are valid or not using our custom logic. Once credentials are valid we are generating our token. We are first adding the basic details like the issuer and the subject and and then adding our custom claims. As I have said that JWT are signed tokens we are signing it with the algorithm we have defined.
SERVICE 1
This service is called by the external client. The client should pass the token obtained from the auth service as a bearer token. Strictly for the sake of simplicity this service is not validating the JWT token and is directly calling the other service.
Alert! 🚨 : Ideally, each service should check if a valid token is provided as a first step in the controller method. We should only skip this check for non protected resources.
@RestController
public class UserController {
@RequestMapping(value = "/user", method = RequestMethod.GET)
public User getUserDetails(@RequestParam String username, @RequestHeader(value="Authorization") String jwt) {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(jwt);
HttpEntity<String> entity = new HttpEntity<>("parameters", headers);
return new RestTemplate().exchange("http://localhost:8082/user/"+username, HttpMethod.GET, entity, User.class).getBody();
}
}
In the above service-1 we are just getting the external call and then calling our internal service also passing the token we have received.
SERVICE 2
A request can come from any service(as in our case it is coming from service 1) or from some external client. To maintain authentication and authorization we are ensuring that each request should have a valid token.
Once the token is validated we are also checking if the user who has obtained the token is the same user for which the request is received. In case they are different we can say that the logged in user (Token owner) is not allowed to fetch data for some other user.
We can also add any number of claims to add additional state information and can validate those claims as per our business rules.
@RestController
public class UserController {
Logger logger = LoggerFactory.getLogger(UserController.class);
@RequestMapping(value="/user/{username}", method = RequestMethod.GET)
public ResponseEntity<User> getUser(@PathVariable(value="username", required = true) String username, @RequestHeader(value="Authorization") String jwt) {
String token = jwt.substring(jwt.lastIndexOf("Bearer ")+7);
try {
JWTVerifier verifier = JWT.require(getAlgorithm())
.withIssuer("pulgupta")
.build();
verifier.verify(token);
DecodedJWT decode = JWT.decode(token);
String username1 = decode.getClaim("preferred_username").asString();
if(!username.equals(username1)){
logger.warn("Token owner and request user are different. Not authorised");
return new ResponseEntity<>(HttpStatus.FORBIDDEN);
}
} catch (UnsupportedEncodingException |
SignatureVerificationException | JWTDecodeException | InvalidClaimException e) {
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
User supplier = new User(username, "pulgupta", "wxyz", "Delhi");
return new ResponseEntity<User>(supplier, HttpStatus.OK);
}
private Algorithm getAlgorithm() throws UnsupportedEncodingException {
return Algorithm.HMAC256("secret");
}
}
In the above code we are using the same algorithm we have used for creating the token. In case there is a algorithm or secret mismatch our JWT decoding will fail. Once the token is decoded we can then fetch our custom claims from JWT and use them as per our business logic.
CONCLUSION
So to sumarise a typical flow in JWT based auth is like:
- A user first has to validate his/her user credentials to obtain a token.
- Without this token user will not be able to call any protected service.
- Each service will check if the token is valid as the first step in the controller.
- In case some user is trying to cheat his way into the system by forging JWT, the JWT validation will fail. This will ensure that the system is not compromised.
- Finally, we should check who is the token owner and if he is allowed to do what he is trying to do.
If we are following these steps we can ensure that each service is able to perform authentication and authorization individually without any external call.
For complete code please visit my Github
