JSON Web Token Cheat Sheet¶
Introduction¶
This cheat sheet provides tips to prevent common security issues when using JSON Web Tokens (JWT).
JSON Web Tokens (JWT) are security tokens for carrying information (claims), often about a user, an application, etc. (subject). JWTs can provide authenticity of the claims (signed JWT) and/or confidentiality of the claims (encrypted JWT). In addition, JWT defines standard claims.
JWTs are used in a wide range of applications such as:
- In OpenID Connect, the ID token is a JWT used to represent the identity and attributes of the connected user.
- In OAuth 2, the access token used to obtain access to a protected resource can be a JWT.
- A JWT is often suggested for “stateless” user sessions. However, this usage is frowned upon.
- A DPoP Proof JWT can be used to prove possession of a private key.
- In SPIFFE, a JWT-SVID can be used to authenticate a workload.
In its most common form (signed JWT), this information is protected by the generating application (issuer) using a signature to ensure it has not been tampered with. This signature prevents attackers, such as a malicious client or user, from forging a token or modifying the claims in an existing token, for example changing the user role from a simple user to an admin or altering the client's login. The JWT can be seen as a protected identity card or certificate about a user, an application, etc. An application (presenter) presents the token to a consuming application (audience) which can verify the token's authenticity and validity and take decisions or actions based on these claims.
JWT can also provide confidentiality of the claims (encrypted JWT). Encryption is currently not treated in this cheat sheet but many aspects of this cheat sheet are applicable to encrypted JWTs.
Token Structure¶
Signed JWTs have the following structure:
{base64url(json(header))}.{base64url(json(claims))}.{base64url(signature)}
The following elements are present in signed JWTs:
- Protected Header: the JWT header contains some information about the token such as the type of token (IANA media type) and the cryptographic algorithms used to protect the token.
- Claims: the content JWT is a list of claims (usually about the subject). See the JWT IANA Registry for a list of standard claims.
- Signature: a signature in JWT is either a public-key digital signature (using a public/private key pair) or a MAC (using a shared secret). The signature protects both the protected headers and the claims.
Example¶
For example, the following example (taken from JWT.IO):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30
The first part (protected header) can be decoded into:
{
"alg": "HS256",
"typ": "JWT"
}
The second part (claims) can be decoded into:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022
}
The last part (signature) guarantees the authenticity of both the header and the claims, either using a public/private key pair (digital signature) or a shared secret (MAC), depending on the alg header value. For our example, it is computed as:
base64url(
HMACSHA256(
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
+ "."
+ "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0",
key
)
)
Considerations about using JWTs¶
Not using JWTs¶
Before using JWTs to solve your problems, you should consider if they are really necessary for your use case.
JWTs are often suggested for “stateless” user sessions. However, if you use JWTs for user sessions, you will need a solution for managing session invalidation. This can be achieved using a deny list of revoked sessions/tokens. If your application implements such a deny list, user sessions won't be completely stateless anymore which might defeat the benefits of stateless sessions. You might want to consider using a plain session system and follow the advices from the dedicated session management cheat sheet.
Public-key Signatures vs. MAC¶
A signed JWT can be authenticated using either a digital signature or a MAC:
- When using a digital signature, the issuer of the token uses its private key to generate a signature. The audience of the token can use the associated public key to verify the authenticity of the token. Whereas the private key must only be known by the issuer, the public key can be public.
- When using a MAC, a shared secret is shared between the issuer and the audience. The same shared secret is used by the issuer to generate the token and by the audience to verify the authenticity of the token.
The two approaches differ on how credentials are managed.
When using a digital signature:
- The issuer can reuse the same public key for many different audiences.
- The audience of the token only need public information to validate the token authenticity which removes the risk of secret leakage by the audience.
- Because the public key does not need to be secret, it can easily be distributed (eg. by publishing it at a public HTTPS URI).
- This makes key rotation simpler as well.
- Traditional digital signature schemes might be broken by post quantum computers in the future. They would need to be replaced with post-quantum digital signature schemes which heavier are traditional signature schemes.
When using a MAC:
- A different secret must be used for each (issuer, audience) pair. If, for example, the same secret is reused for difference audiences, one audience can forge a token (impersonating the issuer).
- Secret keys must obviously not be published at a public HTTPS URI. Some solution for secret distribution and rotation must be found.
- MAC are much faster than digital signatures (but this is usually negligible in practice).
Using a MAC may be interesting in the following cases:
- The issuer of the token is the sole audience of the token. Even in this case, it might be easier to use digital signature for secret rotation/distribution.
- The issuer of the token is the audience. In this case, there is no problem of secret rotation/distribution.
Public-key Signatures¶
| Signature scheme | Identifier | Type | Status |
|---|---|---|---|
| EdDSA | EdDSA, Ed448, Ed25519 | Traditional | Recommended, limited support |
| ECDSA | ES256, ES384, ES512 | Traditional | Recommended |
| RSASSA-PSS | PS256, PS384, PS512 | Traditional | Recommended |
| RSASSA-PKCS1-v1_5 | RS256, RS384, RS512 | Traditional | Not recommended |
| Hybrid ML-DSA / EdDSA | ML-DSA-44-Ed25519, etc. | PQ/T hybrid | Draft |
| Hybrid ML-DSA / ECDSA | ML-DSA-44-ES256, etc. | PQ/T hybrid | Draft |
| ML-DSA | ML-DSA-44, ML-DSA-65, ML-DSA-87 | Post-quantum | Very limited support at best |
Explanations:
- Support for EdDSA in JWT implementations is currently limited.
- Generating ECDSA signatures may be dangerous on embedded systems where the quality of the randomness may be problematic. In this case, you the implementation must use deterministic ECDA as defined in RFC 6979.
- Post-quantum signatures (ML-DSA) or hybrid post quantum signatures are designed to be resistant against quantum computers. However, they produce very large signatures, resulting in very large JWTs. Their usage is probably not justified at the moment unless you need signatures with a long validity.
Key management:
- Do not reuse the key pair for another purpose (eg. for encryption).
- Using the same key for authenticating different types of JWTs is fine as long as this does not introduce a risk of token type confusion.
- Do not publish your private key!
MAC¶
| Signature scheme | Identifier | Status |
|---|---|---|
| HMAC with SHA-2 | HS256, HS384, HS512 | Recommended |
Secret management:
- Do not reuse the same secret for another purpose (eg. for encryption).
- Using the same key for authenticating different types of JWTs is fine as long as this does not introduce a risk of token type confusion.
- Do not reuse the same secret with another audience.
- Do not reuse the same secret with another issuer.
- Do not use a password as MAC secret.
- The secret must be generated using a local, cryptographically secure secret generator.
- The secret must have at least the same size as the output (eg. 256, 384 and 512 bits respectively for HS256, HS384 and HS512).
- Do not publish your secret key!
- The secret must have at least 160 bits of entropy.
- For HMAC, the secret should be at least as long as the output size.
Valid HMAC secret generation example:
import secrets
secret_for_hs256 = secrets.token_bytes(256//8)
secret_for_hs512 = secrets.token_bytes(512//8)
Invalid HMAC secret generation:
import random
import secrets
# Using a password/passphrase is not OK:
bad_secret = b"MyProject2026"
# Using a hardcoded secret is not OK:
bad_secret = urlsafe_b64decode(b'KYkbbclxtjJMiHzoPvuahOfarej0VV-nQZPFxK0hyro=')
# Not a secure randomness source:
bad_secret = random.randbytes(256//8)
# Not enough entropy:
bad_secret = secrets.token_bytes(128//8)
# Not enough entropy for HS512
meh_secret_for_hs512 = secrets.token_bytes(256//8)
Threats on JWTs¶
See RFC 8725 for a discussion on threats and vulnerabilities related to JWT.
Unsecured JWTs¶
Some JWT libraries, used to accept unsecured JWTs by default ("alg":"none"). In this case, an attacker would be able to forge their own JWTs: depending on the application, they might be able to impersonate arbitrary users, obtains arbitrary authorizations, etc.
This issue should now be fixed in JWT libraries.
Mitigation:
- Make sure that
"alg":"none"is not accepted by your JWT parser. It should be disabled by default by recent implementations.
Key type confusion¶
Some JWT implementations would accept to use a public key intended for public-key digital signature as if it was a secret key used for MAC verification. In this context, an attacker could forge a MAC-based JWT by using the public key of the real issuer as if it was a secret key.
This threat is also called “key confusion” or “algorithm confusion”.
Example of legitimate token issuance:
token = jwt.encode(claims, private_key_bytes, algorithm="ES256")
Example of attacker forging a token based on key type confusion:
token = jwt.encode(claims, public_key_bytes, algorithm="HS256")
Example of validation potentially vulnerable to key type confusion:
# If the token is using a MAC, the library might interpret the public key bytes as a MAC secret:
decoded = jwt.decode(token, public_key_bytes, algorithms=jwt.algorithms.get_default_algorithms())
Note: this issue is mitigated in recent versions of the PyJWT library by detecting whether a MAC key appears to be a public key (in PEM of SSH format).
Mitigations (at validation):
- use a library which is not vulnerable to the issue (eg. strong-typing of the type of key);
- chose the key depending on the requested signature algorithm or validate that the key used for validation is consistent with the signature algorithm;
- if possible, hardcode the accepted algorithms and do not mix public-key digital signatures algorithms and MAC algorithms.
Example of validation not vulnerable because MAC algorithms are not accepted:
decoded = jwt.decode(token, public_key_bytes, algorithms=["ES256"])
Example of validation not vulnerable because the key is strictly typed:
from joserfc import jwt, jwk
# {"kty":"EC",
# "crv":"P-256",
# "x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
# "y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0"}
public_key = jwk.import_key(jwk)
decoded = jwt.decode(encoded, public_key)
References:
- Algorithm confusion attacks;
- CVE-2022-29217, Key confusion through non-blocklisted public key formats (PyJWT);
- CVE-2023-48223, JWT Algorithm Confusion in fast-jwt.
JWT revocation¶
Token Status List¶
If revocation of the JWTs by the issuer is needed, the Token Status Lists (TSL) can be used:
- the JWT contains the URI of a TSL;
- the TSL aggregates the revocation status of several tokens in compressed form;
- the consumer of the token can fetch the TSL to obtain the revocation status of the JWT.
The issuer includes a status claim in the JWT. This claims contains the URI of the associated TSL and the index of the status of the JWT within this list:
{
"iss": "https://issuer.example/",
"sub": "NsxuACbpJ9N7Ix96aWrYxHX-EZ4",
"iat": 1783635268,
"nbf": 1783635268,
"exp": 1783653268,
"status": {
"status_list": {
"idx": 6,
"uri": "https://issuer.example/tsl/JAffke55FR5gtJQ_rtktWkSaTlI"
}
}
}
Replay protection¶
JWT denylist¶
In some cases, the consumer of the token might want to maintain a JWT denylist. This might be for example used a simple form of JWT replay protection or as a workaround for the “stateless session” invalidation problem.
A JWT deny list can typically be implemented based on the jti and iss claims:
def revoke_token(claims):
jti = claims.get("jti")
iss = claims.get("iss")
exp = claims.get("exp")
deny_list.insert((jti, iss), exp)
def is_token_revoked(claims) -> bool:
jti = claims.get("jti")
iss = claims.get("iss")
return deny_list.contains((jti, iss))
Depending on the application and the type of JWT, other claims might be more suitable.
Warning: Using the raw JWT or a secure hash of the JWT (SHA-256(token)) as the denylist key is not safe and might expose the application to denylist bypass through JWT malleability. An attacker in possession of a revoked JWT might be able to modify an alternative representation of the JWT that still passes signature verification:
- because of non-strict JWT parsing of the JWT implementation;
- for ECDSA JWTs, because of the malleability of ECDSA signatures.
# Not secure. Might be vulnerable to JWT malleability:
def unsafe_revoke_token(claims):
exp = claims.get("exp")
token_hash = hashlib.sha256(token.encode("utf-8")).digest()
deny_list.insert(token_hash, exp)
Before implementing such a JWT denylist, you should consider whether there is a better solution for your problem:
- Token Status List is a scalable solution for revocation of the JWT by the issuer.
- Freshness and replay protection can often by implementing by using a
noncebound to the session in the JWT claims. This approach is used in OpenID Connect. - Token reuse can be mitigated by using short expiration time in the JWT.
- The risk of token exfiltration can be mitigated by using sender constrained JWT (such a DPoP or TLS-bound JWT).
References¶
Main JWT and JOSE specifications:
- RFC 7515, JSON Web Signature (JWS)
- RFC 7516, JSON Web Encryption (JWE)
- RFC 7517, JSON Web Key (JWK)
- RFC 7519, JSON Web Token (JWT)
- RFC 8725, JWT Best Practices
Some applications of JWTs:
IANA registries:
Attacks on JWT and JOSE:
- {JWT}.{Attack}.Playbook - A project documents the known attacks and potential security vulnerabilities and misconfigurations of JSON Web Tokens.
- JWT.io Discussion Forum (Hosted by Auth0)
Other useful links: