Today, virtually every web developer uses JSON Web Tokens (JWTs) one way or another. OAuth 2.0 and OpenID Connect use them to exchange information between parties. Modern applications use them to keep track of state between requests. Backend services use them to propagate authorization information in a microservice architecture.
In spite of the popularity of JWTs, their security properties are often unknown or misunderstood. How do you choose the signature scheme for a JWT? What other properties should you verify before trusting a JWT? How do you handle key rotation and key management?
The answers to these questions are crucial to ensure the security of the application's architecture. In this article, we go beyond the typical narrative of using JWTs. We look at the hard parts nobody ever talks about, including:
Symmetric JWT signatures
Asymmetric JWT signatures
JWT validation beyond signatures
Cryptographic key management
Using JWTs in practice
In the end, we also provide a cheat sheet on JWT security, to keep track of the best practices we cover here.
NOTE: If you are not familiar with the basics of JWTs, we suggest you read an introduction to JWT before diving into this article.
Symmetric JWT Signatures Screenshot Figure 1 below shows the decomposition of a JWT into its three components. As you can see, the middle part of the token contains the actual data. The header includes metadata about the token, and the signature is there to ensure the integrity. The signature is essential to detect unauthorized tampering with a token.
Figure 1 A JWT signed with an HMAC requires the same secret to verify the signature.
How it works When a service generates a JWT, it also creates a signature. Traditionally, this signature is an HMAC, which uses a particular type of cryptographic functions. Such an HMAC algorithm is indicated with the "HS" prefix, as shown in the sample token above.
The HMAC takes the header, the payload and a secret key as input, and returns a unique signature over all three inputs. This process is illustrated in the left part of Figure 2 below.
Figure 2 This schematic shows how to generate and verify a JWT with a symmetric key.
When a service receives an inbound JWT, it needs to verify the integrity before using the embedded data. To do so, the service uses the same secret key to calculate the HMAC of the JWT. If the resulting HMAC is the same as the signature in the token, the service knows that all three inputs to the HMAC function were the same as before.
However, if the HMACs do not match, something has changed. The secret key is unlikely to change, so something in the inbound JWT has changed. The service does not care what changed. It merely rejects the JWT altogether. This process is shown on the right in Figure 2 above.
Limitations of Symmetric Signatures This signature scheme is straightforward. It is also the typical scheme used to explain JWTs to developers. Unfortunately, symmetric signatures prevent the sharing of the JWT with another service. To verify the JWT’s integrity, all services would need to have access to the same secret key. However, possession of the secret key is enough to generate arbitrary JWTs with a valid signature.
Sharing the HMAC secret with a third-party service creates a significant vulnerability. Even sharing the secret between different services within a single architecture is not recommended. If all services share the same key, the compromise of a single service immediately becomes an architecture-wide problem.
Instead of sharing the secret HMAC key, you can opt for using asymmetric signatures.
Asymmetric JWT Signatures An asymmetric signature uses a public/private key pair. Such a key pair possesses a unique property. A signature generated with a private key can be verified with the public key. And just as the name implies, the public key can be shared with other services. Figure 3 below shows a JWT with an asymmetric signature.
Figure 3 A JWT signed with a private key can be verified with the corresponding public key.
How it works In Figure 4 below, the process of generating a signature is shown on the left. The method of verifying the signature is shown on the right.
Figure 4 This schematic illustrates the process of signing with a private key and verifying with a public key.
As you can see, the issuer of the token only uses the private key. This implies that the private key can be kept in a confidential location, only known to the issuer of the JWT tokens. The public key can be widely distributed, so every consumer of the token can verify its integrity. The algorithms that generate such a signature are indicated with the "RS" prefix, as stated in the sample token shown earlier.
Asymmetric signatures in OpenID Connect OpenID Connect is one of the most common protocols that uses this signature scheme. For example, when you use a "Login with Google" feature, you are using OpenID Connect. At the end of that process, Google provides an identity token to the application. This identity token contains information about your identity with Google. As a result, it tells the application who you are, making OpenID Connect an authentication protocol.
In the OpenID Connect scenario, Google uses their private key to sign the identity token. The application uses Google's public key to verify its integrity, before relying on the embedded data.
Similarly, enterprise deployments using OpenID Connect for single sign-on (SSO) also rely on JWTs. The SSO service signs the tokens with a private key. Every consuming application verifies the integrity using the public key.
The benefits of asymmetric signatures Asymmetric signatures are not limited to OpenID Connect. Every distributed scenario using JWTs benefits from using this signature scheme. For example, in a microservice architecture where JWTs are exchanged, each service can have a public/private key pair. Compared to symmetric signatures, this scheme significantly reduces the impact of a breach of a single service in this architecture.
JWT Validation beyond Signatures Using JWTs securely goes beyond verifying their signatures. Apart from the signature, the JWT can contain a few other security-related properties. These properties come in the form of reserved claims that can be included in the body of the JWT.
The most crucial security claim is the "exp" claim. The issuer uses this claim to indicate the expiration date of a JWT. If this expiration date lies in the past, the JWT has expired and must not be used anymore. A typical example use case is an OpenID Connect identity token, which expires after a set period.
A second related claim is the “iat” claim. This claim indicates when the JWT has been issued. It is often used to enable the consumer of the JWT to decide if the token is fresh enough. If not, the consumer can reject the JWT in favor of a newly issued one.
Third, JWTs can contain the "nbf" claim. This abbreviation stands for "not before." It indicates the point in time when the JWT becomes valid. A JWT can only be accepted if this timestamp lies in the past.
The fourth security-relevant reserved claim is "iss." This claim indicates the identity of the party that issued the JWT. The claim holds a simple string, of which the value is at the discretion of the issuer. The consumer of a JWT should always check that the "iss" claim matches the expected issuer (e.g., sso.example.com).
The fifth relevant claim is the "aud" claim. This abbreviation stands for audience. It indicates for whom the token is intended. The consumer of a JWT should always verify that the audience matches its own identifier. The value of this claim is again a string value, at the discretion of the issuer. In OAuth 2.0 and OpenID Connect scenarios, this value typically contains the client identifier (e.g., api.example.com).
Note that the specification mentions that all of these claims are optional. Nonetheless, it is highly recommended that your application includes them when issuing JWTs. Similarly, their presence must be verified when validating JWTs. Doing so can help prevent abuse when the JWT is exposed one way or another.
Below is a code example of how to verify these claims using the popular “java-jwt” library. As you can see, the library offers dedicated functions to verify these claims. Check your libraries to find out how to optimally handle these claims.
Cryptographic key management One of the most challenging parts of using JWT is handling the cryptographic key material. Cryptographic keys used for signing and encryption need to be rotated frequently. Performing too many operations with a key opens the application up to cryptanalysis attacks. Unfortunately, key rotation is often overlooked in applications using JWTs.
Key rotation is not an easy problem to solve. Rotating keys implies that multiple keys might be in use at the same time. Additionally, it requires the issuer and consumer to retrieve keys dynamically. So let's take a look at how to handle key management for JWTs in practice.
Identifying a key The suite of specifications on JWT provisions a few different options to identify particular cryptographic keys. The most straightforward mechanism is the "kid" claim. This claim can be added to the header of the token. It is intended to contain a string-based key identifier. An example is shown in Figure 5 below.
Figure 5 The "kid" header claim can be used to identify a specific key.
With the key identifier, the consumer of a JWT can retrieve the proper cryptographic key to verify the signature. This simple mechanism works well when both the issuer and consumer have access to the same cryptographic key store. For example, a set of services running within one trust zone can access the same key vault. The key identifier then identifies either a key used for an HMAC or a public/private key pair used for asymmetric signatures.
However, many scenarios require a more dynamic configuration. Imagine having to verify the identity token issued in an OpenID Connect flow. The keys used by the issuer will rotate frequently. If the issuer resides in a different trust zone, it is unlikely that both issuer and consumer have access to the same trusted key store. For those cases, the specification provides a more dynamic configuration mechanism.
Embedding keys A first option to distribute a public key is by embedding it directly into the header of the JWT. The consumer can retrieve the key and use it to verify the signature. The specification provides two mechanisms here. The first is the "jwk" claim, which is designed to hold a public key in the JSON Web Key format. The second is the "x5c" claim, intended to hold a public key in the format of an X509 certificate.
Embedding the key within the token is a straightforward way to enable key distribution. To ensure the security of this mechanism, the consumer of the JWT needs to restrict which keys it accepts. Failure to do so allows an attacker to generate tokens signed with a malicious private key.
An overly permitting consumer would merely use the embedded public key to verify the signature, which will be valid. To avoid such issues, the consumer needs to match the key used against a set of explicitly whitelisted keys. In case the key comes in the form of an X509 certificate, the consumer can use the certificate information to verify the authenticity.
Distributing keys The second option to distribute keys dynamically is using key URLs. The "jku" claim is intended to hold a URL, which points to a file containing keys in the JSON Web Key format. This mechanism is often used in an OpenID Connect context for automatic key retrieval. An example can be found on Google’s API endpoint. Similar to before, the "x5u" claim is intended to do the same for X509-formatted keys.
The use of a key URL yields more flexibility, as well as smaller token sizes. Similar to embedding keys, the consumer will have to take a few precautions into account. The consumer needs to ensure that the key URL or keys belong to a trusted party. One option is to whitelist an entire key URL, or at least the domain of a key URL. Similar to embedded keys, x509-formatted keys can be authenticated using the properties embedded in the certificate.
Note that a key URL points to a file hosted on a server somewhere. Such a file may contain more than one key definition. To ensure the consumer can identify the right key, a key URL is typically combined with the "kid" claim. In that case, the key identifier points to one of the keys in the key file.
Finally, the consumer may opt to use a caching mechanism for the key URLs, to improve performance.
Using JWTs in practice So far, we have talked about the technical properties of JWT tokens. However, using JWTs in practice requires careful consideration of the security properties of a JWT.
In most cases, JWTs are used in the form of bearer tokens. A bearer token is a token that can be used by anyone who possesses it. Consequently, obtaining a JWT suffices for an attacker to start abusing the privileges associated with that token. In this final section, we will briefly highlight a few use cases.
JWTs in OpenID Connect We have mentioned the use of JWT in OpenID Connect before. The provider issues an identity token to the client. That identity token contains information about the user's authentication with the provider. The identity token is a JWT token, signed with the provider's private key.
OpenID Connect went through great lengths to improve the security properties of the identity token. For example, the protocol mandates the use of the "exp," "iss" and "aud" claims. Additionally, the token includes a nonce to prevent replay attacks. Because of these requirements, abusing a stolen identity token becomes hard or even impossible.
JWTs as OAuth 2.0 access tokens An OAuth 2.0 access token is another good use case of a JWT. Such an access token gives a client application access to a protected resource, such as an API. OAuth 2.0 access tokens come in two flavors: reference tokens and self-contained tokens.
A reference token points to server-side metadata, kept by the authorization server. A reference token functions as an identifier, much like a traditional session identifier.
A self-contained token comes in the form of a JWT. It contains all the metadata as the payload. To protect the data, the issuer signs the token using a private key.
Traditional OAuth 2.0 tokens are bearer tokens. If one becomes compromised, it can be used without restrictions by whoever possesses it. A compromised reference token can be revoked by the authorization server. For self-contained tokens, revocation is a lot trickier.
Therefore, it is strongly recommended to keep the lifetime of access tokens as short as possible. Token lifetimes of minutes or hours are quite common. Lifetimes of days or months are not recommended. If possible, short-lived access tokens should be combined with refresh tokens to improve security.
JWTs as session objects Protocols such as OpenID Connect and OAuth 2.0 actively try to address the weaknesses of JWTs. Unfortunately, we also observe many applications that incorporate JWTs into their architecture, without taking these precautions into account.
A concrete example is an application using JWTs to store authorization state on the client. This enables the use of a stateless backend, which makes deployment significantly easier.
However, such a client-side token is a bearer token. Not having a short lifetime or revocation mechanism in place makes such a scenario extremely vulnerable.
JWT Security Best Practices As you have seen in this article, there's a lot more to JWTs than merely using a symmetric signature. Most JWT deployments require the use of asymmetric signatures to ensure security.
Additionally, verifying the signature of an incoming JWT is only the first step. Next, the consumer has to check the reserved "exp" and "nbf" claims to ensure that the JWT is valid. The "iss" and "aud" claims need to be verified to ensure the JWT is used in the proper context.
Finally, using JWTs requires you to set up proper cryptographic key management. JWTs offer a variety of options to manage keys. One way is to identify a particular key using an identifier. More advanced options enable the embedding of a key in the token, or the retrieval of a key from a dedicated key URL. Regardless of the mechanism, the consumer always needs to verify the validity of the key before trusting it.
We have covered much ground in this article. This JWT Security Cheat Sheet provides an overview of all these best practices. It allows you to keep track of these things while you're building your application. And to learn more about securing APIs using open standards protocols, read the white paper on How to Extend Identity Security to your APIs.