Using JWTs and OAuth2 to Secure APIs and Microservices
In this post we specifically discuss usage of JSON Web Tokens (JWT, pronounced "jot") with OAuth2. JWTs can certainly be used without OAuth2, by utilizing any other authentication method (e.g. Basic Auth over HTTPS), but in this opinionated post we assume OAuth2 is usually a preferred method for performing API client authentication and are mostly interested in how using JWT can improve an OAuth2 implementation.
We further assume that the reader is familiar with OAuth2. If this is not the case, you may want to consult with Alex Bilbie's wonderful OAuth2 reference.
Shortcomings of OAuth2's Opaque Bearer Tokens
Various OAuth2 workflows all authenticate API clients and if auth passes, a server issues API client a bearer token. API clients use the bearer token to make subsequent API calls when accessing protected API endpoints.
OAuth2 spec doesn't define the format of the bearer token or assign it any meaning. In a lot of legacy implementations, bearer token is just a unique string. To validate it (and determine any scopes associated with the token) API endpoint may need to verify the token, provided by a client, using one of two means:
- Calling Authorization service and supplying the token. This leads to a network sub-call for every API call.
- Assuming API Gateway is always in front of it, validating the token for the API at every request. This approach will usually lead to a data-lookup by the API gateway for each API call.
In the two scenarios, either service itself or the API Gateway are performing a database lookup to validate the token, at every API call. The lookup can be cached, of course. Regardless, we are either sacrificying scalability (server memory used for caching is limited) or speed (network calls can be expensive).
JWT tokens give us a way of validating tokens that use some CPU but do not require any RAM or a network call. Since CPU can easily be scaled horizontally it can be a very interesting solution, in many cases.
Using JWT for Token Validation
Instead of a opaque string, a bearer token can be a JSON Web Token (JWT).
A JWT is basically a base64-encoded JSON object represented as a string of
characters. Since the token is actually a JSON object, it can carry all kinds of
information ("claims"). Specifically, JWT can carry such "claims" as:
client_id of the API caller and all OAuth2 scopes associated with
How can we trust that information in JWT is valid and a client is not claiming something it is not authorized for, or pretending to be someone else? We can trust this because JWT tokens are cryptographically signed.
To be very clear: JWT tokens are not encrypted! A JWT can be decoded by anybody. Which means: you should never include private information in it, i.e. never encode passwords or secrets in a JWT! However you can verify whether JWT is properly signed by an OAuth2 server. This can be done in two ways:
- Using a shared secret key (symmetric approach)
- Using private/public key combo (assymetric approach).
The latter is preferred. In the assymetric approach, OAuth2 Authorization Server signs JWT token with a private key. All your API endpoints can possess corresponding public key (which is not secret and can be widely distributed). Using the public key, API endpoints can verify that the JWT was indeed issued by a trusted authentication server (e.g. OAuth2 server) and therefore claims in the token (the JSON object) can be trusted as made by the authorized server.
As you may have already guessed, the huge benefit of the JWT approach is its scalability: an API endpoint verifying the JWT only needs to posess proper public key, but doesn't need to look anything up in a data storage or use memory to cache lookups. Furthermore, public keys are not secrets and don't need to be secured in any special way, making logistics of the verification easy.
JWT tokens can be an extremely good solution for securing inter-service communications in a Microservice Architecture, where speed and scalability can be of paramount importance. Since JWTs can be issued by any authentication server/process, using JWTs also allows entirely bypassing an API Gateway for inter-microservice communications if and when appropriate.
JWT Criticism and Security Considerations
JWTs are not without limitations. Since API endpoints don't consult a central server, central revocation of credentials (an important feature of OAuth2) can be challanging. JWT tokens do have expiration time and are not issued in perpetuity, but if you detect that a set of tokens are compromised, triggering an immediate revocation of those tokens across a distributed network can be a non-trivial effort, at least: until the tokens expire.
Due to this limitation, it is recommended that expiration times of JWT tokens issued be set to reasonably short periods. JWT supports token expiration out of the box using "exp" claims. How short the expiration times should be will depend on your use-cases and requirements.
Another notable capability that JWT spec offers to enhance security of the
tokens is the notion of "jti" (JWT ID)
also known as "JWT nonces". A
jti claim is basically a globally unique
identifier of a JWT token. It is optional, but if we include a
jti in every
token issued, we will be able to identify individual tokens. We can then create
a blacklist of jtis when and if we learn that the specific JWTs are compromised.
This will allow clients validating the tokens to reject compromised tokens by
consulting with the master blacklist of compromised tokens. A jti can be
any globally unique identifier. A good and easy-to-generate candidate for
such identifier can be
uuid version 4.
At the first glance, notion of a master blacklist of compromised tokens may feel like something that destroys the main benefit of JWTs: decentralized validation. While jti validation can indeed reduce decentralization of JWT validation, in general, we can greatly mitigate its negative affects with sensible caching. For instance, you can utilize following two approaches to avoid the need of consulting any central authority per each client request:
- Keep locally cached list of blacklisted JWTs. If a central authority can know and reach all 'local' validators (e.g. microservices) it can push the new list to them, when new entries are added. If not, utilize the method #2.
- Keep locally cached list of blacklisted JWTs. Update this list as frequently as it makes sense to your context. In most cases, you may be able to refresh caches every few minutes, only giving compromised tokens very short time during which they can be used.
Please note that while first method is harder to implement (central authority
needs to know of and be able to update all local validators) it has a benefit of
zero-delay invalidations. In the second method there is some lag, but you can
tweak its duration based on your needs. In both scenarios you can increase
expiration time of JWT tokens, if needed, and it will be more secure compared to
when you don't use 'jti' nonces. This is true because when using
revocation request can propagate quickly and you are not relying solely on JWTs
expiring to remove compromised ones.
Important Optimization: if you implement a local cache of blacklisted JWT tokens, make sure to regularly prune it by deleting expired tokens. This can greatly help you keep the cache sizes reasonable and manageable.