This is a translation of an article I wrote for our internal knowledge base at work, and that we later decided to publish (in French).
This article's goal is to present what JWTs are, whenever you face them. As we'll see, you won't deliberately choose to use JWTs in a project, and more importantly: you won't use JWTs as session tokens.
What is it?
JSON Web Token (JWT) is a compact, URL-safe means of representing data to be transferred between two parties. The data is encoded as a JSON object that can be signed and/or encrypted.
This is, paraphrased, the definition from the IETF standard that defines it (RFC 7519).
What's the point? What's the use case?
So the goal is to transfer data, with some guarantees (or none, by the way): authenticity, integrity, even possibly confidentiality (if the message is encrypted). There are therefore many possible uses.
JWT is thus used in OpenID Connect to encode the ID Token that forwards to the application information on the authentication process that took place at the identity server. OpenID Connect also uses JWT to encode aggregated claims: information from other identity servers, for which we'll want to verify the authenticity and integrity.
A JWT might be used to authenticate to a server, such as with the OAuth 2 JWT Bearer (RFC 7523).
Still in OAuth 2 land, access tokens could themselves be JWTs (RFC 9068), authorization request parameters could be encoded as a JWT (RFC 9101), as well as token introspection responses (IETF draft: JWT Response for OAuth Token Introspection), and finally dynamic client registration uses a JWT to identify the software of which an instance attempts to register (so-called software statements of RFC 7591).
How does it work?
A JWT is composed of at least 2 parts, separated with a .
(dot), the first one always being the header. Each part is always encoded as base64url, a variant of Base 64 with the +
and /
characters (that have special meaning in URLs) replaced with -
and _
respectively, and without the trailing =
.
There are two types of JWTs: JSON Web Signature (JWS, defined by RFC 7515), and JSON Web Encryption (JWE, defined by RFC 7516). The most common case is the JWS, composed of 2 or 3 parts: the header, the payload, and optionally the signature. JWEs are rarer (and more complex) so I won't talk about them here.
The header, common to both types, describes the type of JWT (JWS or JWE) as well as the different signature, MAC, or encryption algorithms being used (codified by RFC 7518), along with other useful information, as a JSON object.
In the case of JWS, we'll find the signature or MAC algorithm, possibly a key identifier (whenever multiple keys can be used, e.g. to allow for key rotation), or even a URL pointing to information about the keys (in JWKS format, defined by RFC 7517), etc.
In the case of JWS, the payload will generally be a JSON object with the transfered data (but technically could be another JWT).
The third part is the signature or MAC. This part is absent if the header says the JWT is unprotected ("alg":"none"
).
For debugging, one can use the JWT Debugger by Auth0 to decode JWTs (beware not to use it with sensitive data, only on JWTs coming from test servers).
⚠️ JWT being almost always used in security-related contexts, handle them with care, specifically when it comes to their cryptographical components.
One MUST use dedicated libraries to manipulate JWTs, and be careful to use them correctly to avoid introducing vulnerabilities.
RFC 8725 has a set of best practices when manipulating and using JWTs.
Criticism
Numerous security experts, among them cryptographers, vehemently criticize JWTs and advise against their use.
The main criticism relates to its complexity, even though it could look simple to developers:
- first, you need to know how to decode UTF-8 and JSON ; that's as many sources of bugs (and potential vulnerabilities).
- and of course because it's a generic format capable of signing and/or encrypting, or even not protecting anything at all (
"alg":"none"
), with a list of supported algorithms as long as your arm, you have to handle many cases (even if only to reject them).
As a result, a number of vulnerabilities have been identified; among them (identified as soon as March 2015):
- As the JWT itself declares the algorithm used to sign or encrypt it, software that receives it needs to partly trust it, or correctly check the used algorithm against a list of authorized algorithms. Because of its apparent simplicity, many libraries came out that didn't do those necessary checks and readily accepted unprotected JWTs (
"alg":"none"
), allowing an attacker to use any JWT, without authenticity or integrity check. And as incredible as it may seem, we still find vulnerable applications nowadays!
Note: in the same way, the header can directly include the public key to be used to verify the signature. Using it will prove the integrity of the JWT, but not its authenticity as the signature could have been generated by anyone.
- Another attack involes using the public key intended to verify an asymmetric signature (
"alg":"RS256"
or"alg":"ES256"
) as a MAC key ("alg":"HS256"
): the application receiving the JWT could then mistakenly validate the MAC and allow the JWT in. Anybody could then create a JWS that would be accepted by the application, when that one thinks it's verifying an asymmetric signature.
This vulnerability could be due to a misuse of the library used to verify JWTs, but also in some cases directly to its API that cannot tell between a public key and a shared secret (generally for the sake of making it easy to use).
Aside: despite ID Tokens in OpenID Connect being JWTs, you won't actually need to verify their signature as you generally get them through HTTPS, that already guarantees authenticity and integrity (and confidentiality), which saves us from a whole class of vulnerabilities.
Another criticism is due to the misuse of JWT, most often by ignorance or lack of expertise in software security: validity of a JWT is directly verifiable, without the need for a database of valid tokens or a validation service (authenticity and integrity are verifiable, so the validity period contained within in the JWT are reliable), but it makes the JWT impossible to revoke (unless you add such a mecanism –possibly based on the jti
claim, initially designed to protect against replay attacks– going against the whole reason for which JWT was chosen in the first place). If a JWT is used as a session token for example, it then becomes impossible to sign out or terminate a session. In most use cases (in the specifications), a JWT is validated and used as soon as it's received from the issuer, so revocation is not even an issue. It's when a JWT is stored by the receiver for a later use that the problem arises (such as with a session token or an access token).
Some articles critical of JWT:
- JWT should not be your default for sessions (by Evert Pot, developper)
- Why JWTs Suck as Session Tokens (by Okta, vendor of an identity management platform)
- section "JSON Web Tokens" of "API Tokens: A Tedious Survey" (by Thomas H. Ptacek, security researcher)
- Alternatives to JWTs (by Scott Brady, engineering manager specializing in identity management systems)
- Stop using JWTs for sessions and Stop using JWT for sessions, part 2: Why your solution doesn’t work (on a web site surprisingly without HTTPS)
- No Way, JOSE! Javascript Object Signing and Encryption is a Bad Standard That Everyone Should Avoid (by Scott “CiPHPerCoder” Arciszewski, cryptographer)
- How to Write a Secure JWT Library If You Absolutely Must (by the same author)
Top comments (4)
Very good and valid points! I would summarise this as: "JWT's can be used in a secure way, but can also be implemented in a totally incorrect way".
As in many technologies, the technology itself doesn't guarantee security, it is the implementation that matters.
Nice overview with many interesting things to consider.
I do disagree with you on this part however:
Here HTTPS only guarantees that the ID Token has been transmitted securely between the end user's user agent and the OpenID Connect client (website). You must still verify the JWT in order to confirm it was issued legitimately from your trusted identity provider. Omitting this introduces many major security risks.
Which flow are you talking about? With the authorization code flow (which you should use over anything else), the ID Token comes right from the IdP in the token response, so HTTPS guarantees both authenticity and integrity (and confidentiality).
If you're using an hybrid flow or implicit flow (but you shouldn't) then I agree you must validate the ID Token signature, as it comes from an insecure channel (redirect or possibly form post)
You are right with respect to the authorization code flow, and that is indeed the flow that should be used always. I checked the specifications and it even explicitly mentions that there is no need to verify the JWT signature. To be honest, I never really realized this.