We use JSON Web Tokens quite a lot in the OAuth and OpenID Connect world. We’re so used to them that we often don’t pay much attention to how they’re actually used. The general opinion is that they’re good for being used as ID Tokens or Access Tokens and that they’re secure - as the tokens are usually signed or even encrypted. You have to remember though, that JWT is not a protocol but merely a message format. The RFC just shows you how you can structure a given message and how you can add layers of security, which will protect the integrity and, optionally, the content of the message. But JWTs are not secure just because they are JWTs, it’s the way we use them that determines whether they are secure or not.
In this article we would like to show you some best practices for using JWTs, so that you can maintain a high level of security in your applications. These practices is what we recommend at Curity and is based on community standards written down in RFCs as well as our own experience from working with JWTs.
If you’re new to JWTs, here’s a quick wrap-up. A JSON Web Token (JWT, pronounced “jot”) is a compact and url-safe way of passing a JSON message between two parties. It’s a standard, defined in RFC 7519. The token is a long string, divided into different parts separated with dots, and each part is base64 encoded. What parts the token has depends on the type of the JWT: whether it’s a JWS (a signed token) or a JWE (an encrypted token). If the token is signed it will have three sections: the header, the payload and the signature. If the token is encrypted it will consist of five parts: the header, the encrypted key, the initialization vector, the ciphertext (payload) and the authentication tag. Probably the most common use case for JWTs is to use them as Access Tokens and ID Tokens in OAuth and OpenID Connect flows, but they can serve different purposes as well.
JWTs are by-value tokens. This means that they contain data. Even if you can’t read that data with your own eyes, it’s still there, and is quite easily available. Whether it’s a problem or not depends on the intended audience of the token. An ID Token is intended for the client’s developers. You expect it to be decoded and its data used by the client. An Access Token, on the other hand, is intended for the API developers. The API should decode and validate the token. But if you issue JWTs to your clients to be used as Access Tokens you have to remember that client developers will be able to access the data inside of that token. And believe us - if they can, they will. This should make you consider a few things:
- Some developers can start using the data from the JWT in their applications. This isn’t a problem in itself, but can explode the minute you decide to introduce some changes to the structure of the data in your JWT. Suddenly many integrating apps can stop working as they won’t be prepared for the new structure (e.g. some fields missing, or change to max length of the field).
- As everyone can read what is inside the token, privacy should be taken into account. If you want to put sensitive data about a user in the token, or even Personally Identifiable Information, remember that anyone can decode the token and access the data. If such information can’t be removed from the token you should strongly consider switching to the Phantom Token approach or the Split Token approach, where an opaque token is used outside of your infrastructure and JWTs are only available to your APIs, thanks to integration with an API Gateway.
- Users private data is not the only information that can be leaked in a JWT. You should make sure that you don’t put any valuable data about your API in the token. Anything that would help attackers to breach your API.
It’s also good to keep it mind, that Access Tokens are most often used as Bearer tokens. That means that you accept the
token from whoever presented it to you - it’s pretty much like paying with cash in a shop. If you a find 10$ bill lying
in the street, and pay with it for a coffee, it will be accepted, as long as it’s a genuine banknote. The same applies
to bearer Access Tokens. If that could pose problems to your application, you can change the Bearer token into a Proof of
Possession token (a PoP token) by adding a
cnf claim - a confirmation claim. The claim can e.g. contain a fingerprint
of the clients certificate, which can then be validated by the resource server.
Whether the token is signed (a JWS), or encrypted (a JWE) it will contain an
alg claim in the header, that indicates
which algorithm has been used for signing or encryption. When verifying / decrypting the token you should always check the
value of this claim with a whitelist of algorithms that your system accepts. This mitigates an attack vector where someone
would tamper with the token and make you use a different, probably less secure algorithm to verify the signature or decrypt
the token. Whitelisting algorithms is preferred over blacklisting, as it prevents any issues with case-sensitivity. There
were attacks on some APIs which leveraged the fact the alg
noNe was interpreted as
none but was not discarded by the
The special case of a
none value in the
alg claim tells clients that the JWS is actually not signed. This option is
not recommended and you should be absolutely sure what you’re doing if you want to enable unsigned JWTs. This would usually
mean that you have strong certainty of the identity of both the issuer of the token and the client which uses the token,
and you’re absolutely sure that no party could have tampered with the token.
The JWA RFC lists all available algorithms that can be used to sign or encrypt JWTs. It also tells you which algorithms are recommended to be implemented by clients and servers, given the current state of knowledge on cryptography security.
When signing is considered, currently the most recommended algorithm is ES256 (The Elliptic Curve Digital Signature Algorithm (ECDSA) using P-256 and SHA-256), although still the most popular one is RS256 (RSASSA-PKCS1-v1_5 using SHA-256). The former one is a lot faster than the latter, which is one of the main reasons for stronger recommendation. The latter has been around much longer and offers better support in different languages and implementations. Still, if your setup enables this, and you’re pretty sure that your clients will be able to use it, you should go for the ES256.
If you really need to use symmetric keys, then HS256 (HMAC using SHA-256) should be your choice - though using symmetric keys is not recommended, take a look at When to use symmetric signing to learn why.
The rule of thumb is - you should always validate an incoming JWT. You should do it, even if you’re working on an internal network - where the Authorization Server, the Client and the Resource Server aren’t connected through the Internet. You shouldn’t rely on your environment settings to be part of your security scheme. If you move your services to a public domain, the threat model will change and you will have to remember to update your security measures - experience shows that this is very often overlooked. Moreover, implementing token validation from the start will guard your from situations where someone manages to break into your network, or you would have a malicious actor in your organization.
The one case when you could consider omitting checking the signature of the token is when you first get it in the response from the token endpoint of the Authorization Server using TLS. You should definitely validate a token if using the implicit flow, and the token is sent back to the client by means of a redirect URI, as in such case there is a greater risk of someone tampering with the token before you manage to retrieve it.
Another claim that you should always check against a whitelist is the
iss claim. When using the JWT you should be
sure that it has been issued by someone you expected to issue it. This is especially important if you adhere to another
good practice and dynamically download the keys needed to validate / decrypt the tokens. If someone should send you a
forged JWT, put their issuer in and you then download keys from that issuer, then your application would validate
the JWTs and accept them as genuine.
This good practice can also be explained in other words: if the token contains the
iss claim you should always confirm
that any cryptographic keys used to sign or encrypt the token actually belong to the issuer. How to verify this will be
different for different applications. E.g. If you’re using OpenID Connect the issuer must be a URL using the https scheme.
This makes it much easier than to confirm the ownership of the keys or certificates. Thus, it’s good practice to always
use such URLs as the issuer value. If this is not the case, you should make sure to get to know how to check this ownership.
Also, remember that the value of the
iss claim should match exactly the value that you expect it to be. If you
expect the issuer to be
https://example.com, this is not the same as
The resource server should always check the
aud claim in the token and confront it with a whitelist. The server should
expect that the token has been issued for an audience, which the server is part of. It should reject any requests that
contain tokens intended for different audiences. This helps to mitigate attack vectors where one resource server would
obtain a genuine Access Token intended for it, and then use it to gain access to resources on a different resource server,
which would not normally be available to the original server.
An ID Token must contain the client ID in the
aud claim (though it can also contain other audiences). You expect the token
to be decoded by the client so it can use the data inside of it. This token should not be passed to anyone else.
For Access Tokens it’s a good practice to use the URL of the API that they are intended for.
JWTs can be used as Access Tokens or ID Tokens or sometimes for other purposes it’s thus important to differentiate the different types of the tokens. When validating JWTs always make sure that they are used as intended. E.g. that you won’t accept an ID Token JWT as an Access Token. This can be achieved in different ways and will depend on the implementations you use. Here are some examples:
- You can check the scope of the token. ID Tokens don’t have scopes, so checking whether an Access Token has any or a concrete scope will help you differentiate them.
- As noted before, the tokens should have different values of the
audclaim. If this is the case, you can use the value of that claim to check the token type.
- Curity Identity Server sets a
purposeclaim on the token, with values of either
- Some Authorization Servers might set the not-yet-standardized
typclaim in the token header to
at+JWTfor Access Tokens. If your server supports that, it can be used to differentiate the tokens.
JWTs are self-contained, by-value tokens and it is very hard to revoke them, once issued and delivered to the recipient. Because of that, you should use as short expiration time for your tokens as possible - minutes or hours at maximum. You should avoid giving your tokens expiration times in days or months.
Remember that the
exp claim, containing the expiration time, is not the only time-based claim that can be used for verification.
nbf claim contains a “not-before” time. The token should be rejected if the current time is before the time in the
nbf claim. Another time-based claim is
iat - issued at. You can use this claim to reject tokens which you deem too old to
be used with your resource server.
When working with time-based claims remember that server times can differ slightly between different machines. You should consider allowing a clock skew when checking the time-based values. This should be values of a few seconds, and we don’t recommend using more than 30 seconds for this purpose, as this would rather indicate problems with the server, rather than a common clock skew.
In case of a signed JWT - a JWS - you have to remember that the signature is used to sign not only the payload of the token but also the header. Any change in the header or the payload would generate a different signature. This doesn’t even have to be a change in the values of the claims - adding or removing spaces or line breaks will also create a different token signature.
It’s worth to note that in order to mitigate a situation where two tokens would be created with exactly the same signature
(so two tokens created in the same second, for the same client and user, with the same scope, etc.) many Authorization
Servers add a random ID of the token in the
jti claim. Thanks to this you can be sure that two different tokens will
never have the same signature.
Signatures require keys or certificates to be properly validated. Those keys or certificates can be obtained from the Authorization Server in a few different ways. You can, e.g. get the keys from the AS in an onboarding process, and make sure that all of your resource servers have access to those keys. This however creates a problem when the keys or certificates change. That’s why it’s good practice to always use an endpoint and dynamically download the keys or certificates from the Authorization Server (caching responses accordingly to what the server returns in the cache control headers). This allows for an easy key rotation, and any such rotation will not break your implementation.
If the keys or certificates are sent in the header of the JWT, you should always check them against a whitelist of keys, or validate the trust chain in case of certificates.
Remember also that the
alg in the header must be whitelisted (as explained here and
you should verify whether the issuer is the owner of the keys / certificates.
The rule of thumb here is - try to avoid using symmetric signing. Nowadays there are probably not many use cases where you would have to use symmetric signing instead of asymmetric. When using symmetric keys then all the parties need to know the shared secret. When the number of involved parties grows it becomes more and more difficult to guard the safety of the secret, and to replace it, in case it is revealed.
Another problem with symmetric signing is the proof of who actually signed the key. When using asymmetric keys you’re sure that the JWT was signed by whoever is in possession of the private key. In case of symmetric signing, any party that has access to the secret can also sign the tokens.
If, for some reason, you have to use symmetric signing try to use ephemeral secrets, which will help increase security.
The OpenID Connect standard introduces Pairwise Pseudonymous Identifiers (PPID) that can be used instead of a plain user ID. A PPID is an obfuscated user ID, unique for a given client. This helps to improve users’ privacy. Especially if you use some sensitive data as the user ID, e.g. e-mail or the Social Security Number. Thanks to PPID, the client can still differentiate the user, but will not get any excess information. Have a look at the Pairwise Pseudonymous Identifiers article to learn more.
There is an increasing number of frontend developers who claim JWTs have some benefits for use as session retention mechanism, instead of session cookies and centralized sessions. This should not be considered as good practice. JWTs were never considered for use with sessions, and using them in such a way may actually lower the security of your applications. If you’re interested to know what the exact reasons against such use of JWTs are, have a look at these articles:
It’s important to remember that JWT safety depends greatly on how the tokens are implemented and used. Just because a JWT contains a cryptographic signature it doesn’t automatically mean that it’s safe, or that you should blindly trust the token. Unless good practices are observed you can quite easily become the victim of an attack.
The good practices outlined in this article are true at the time of writing. You should remember that security standards and the security levels of our cryptography can change quite rapidly and it’s good to keep an eye on what is happening in the industry. You can follow any changes in the RFCs which talk about the good practices for JWTs: in RFC 8725 JSON Web Token Best Current Practices and in RFC 7518 JSON Web Algorithms (JWA).