I am famously not a fan of JSON Web Tokens (JWT).
Like most cryptography and security experts familiar with JWT, I would much rather you use something else if you can. I even proposed a secure alternative called PASETO in 2018 (with an optional extension called PASERK to handle advanced use-cases; namely key-wrapping and asymmetric encryption).
PASETO solves the exact same use-case as JWT, but provides a rationale for its design decisions and goes out of its way to prevent security issues at the specification level (which, if done correctly, will prevent security issues from popping up in implementations).
If you can get away with never using JWT, all the better.

Unfortunately, you don’t always have a choice in the matter.
Sometimes you need a JWT parser for the sake of interoperability or backwards compatibility. Other times, you need one for political reasons; i.e. because JWT has an IETF RFC (which is necessary for some companies to consider something an Internet Standard at all) and its competing designs (Macaroons, PASETO, etc.) do not.
Repeat after me: all technical problems of sufficient scope or impact are actually political problems first.
Eleanor Saitta
Over the years, many security professionals have attempted to enumerate the badness of the JWT ecosystem. In 2020, the IETF published RFC 8725, which offers so-called best practices for JWT. There was even a two hour workshop at the AppSec Village at DEFCON 31 that tackled JWT security in depth, and a Black Hat USA 2023 talk advertising new JWT attacks. See also: this JWT Vulnerabilities Cheat Sheet.
All of these efforts fall short, despite the excellence of the people behind them, simply because the number of ways a developer can screw up JWT implementations without violating the standard is uncountable. Attempting to account for them all is a doomed effort–like trying to predict the wacky shit that a QAnon believer will say next.
To wit: Using the Key ID header to reintroduce the RS256/HS256 algorithm confusion vulnerability in scripting languages with frameworks that emphasize dependency injection and having one configuration object. The JWT Best Practices RFC is completely silent on these kinds of misuse.
As one of JWT’s most vocal critics, and a cryptography/security nerd who obsesses over making tools that are easy to use and hard to misuse, I thought I would take a stab at the opposite of the above approaches. Rather than list off the known ways to implement JWT insecurely and muse about mitigation strategies, I will instead offer my strategy for building a JWT library from better security principles.
Building a Proactively Secure JWT Library
Before we begin, we need to establish some ground rules. Tenets, if you will.
These rules must not be broken without sufficient justification at any point. If they are broken in any way, for whatever reason, you must be very loud and draw attention to the fact that you’re violating one of them, state your reasons for doing so, and let the users of your library decide whether to continue trusting you or not.
- A key should always be considered to be the raw key material alongside its parameter choices.
Keys are not passwords. Keys are not just raw bytes or strings.
Keys are objects with a specific hard-coded set of parameters in addition to the raw keying material. - Never, ever trust the header for a security decision.
- You don’t need to toss in the kitchen sink while you’re at it.
If you don’t need it, don’t implement support for it. If you only need JSON Web Signatures, your code should not include anything concerning Elliptic Curve Diffie-Hellman. - Only use cryptographer-recommended algorithms.
One Parser Per Algorithm
Every single JWT Parser you implement support for should only support one algorithm.
For example, you might need a PS256 parser for authentication and an HS256 parser for tamper-proof cookies. Do not make them the same object.
Each parser should have a finite set of trusted keys. Each key must only be used with that specific parser class, and not be accepted in any other. In the simplest case, you only have one key per object.
This is an inversion of the typical JWT library user experience, which consists of a single function call with optional parameters specifying the allowed algorithms (yes, plural) and a list of keys.
Don’t Do This
/* This is an example of what to NOT do */
export function decodeJwt(token, key, algs = []) {
const header = getJwtHeader(token)
/* some libraries omit this check, but it's still
kind of YOLO */
if (!(header.alg in algs)) {
throw new Error("invalid header algorithm")
}
const sigAlg = getSignatureAlgorithm(header.alg)
if (!sigAlg.verify(key, token)) {
throw new Error("invalid token")
}
/* Oops. What if sigAlg and key don't match? */
return getJwtClaims(token)
}
Do This Instead
export class JwtParser {
constuctor(alg, key) {
this.sigAlg = getSignatureAlgorithm(alg)
this.key = key
}
decode(token) {
/* Notice how this.sigAlg was decided before
decode() is called? */
if (!this.sigAlg.verify(this.key, token)) {
throw new Error('invalid token')
}
return getJwtClaims(token)
}
}
If you need JWK support, you can be even fancier:
export class JwtParser {
constuctor(alg, keys = {}) {
this.sigAlg = getSignatureAlgorithm(alg)
this.keys = {}
for (let k of keys) {
this.addKey(k, keys[k])
}
}
addKey(keyId, key) {
if (key.alg !== this.alg) {
throw new Error('invalid algorithm for key')
}
this.keys[keyId] = key
return this
}
decode(token) {
const keyIds = Object.keys(this.keys)
if (keyIds.length === 0) {
throw new Error('no keys defined')
}
const header = getJwtHeader(token)
let keyId
/* keyid support */
if ('kid' in header) {
if (!(header.kid in this.keys)) {
throw new Error('kid not found')
}
keyId = header.kid
} else {
/* otherwise, only one key is tolerable */
if (keyIds.length !== 1) {
throw new Error('invalid config')
}
keyId = keyIds[0]
}
/* Either way, we land at one key */
const key = keys[keyId]
/* Notice how this.sigAlg is decided before
decode() is called? */
if (!this.sigAlg.verify(key, token)) {
throw new Error('invalid token')
}
return getJwtClaims(token)
}
}
With this setup, you will verify each token with the correct algorithm and a key intended for that algorithm. This makes it hard for an attacker to influence the behavior of your library, or produce an unintended result.
You May Route Tokens to the Respective Parser
What if I need to support multiple algorithms?
Obvious Question
It’s acceptable to create a JWT Router class that reads the header off a token, verifies that you have a Parser class for that specific header algorithm (and fails closed if you don’t), and then passes it to the corresponding Parser.
Each Parser must not accept keys for the wrong algorithm. You should write unit tests that verify this behavior.
class JwtParserRouter {
constructor() {
this.parsers = {}
}
addParser(alg, parser) {
if (alg.toLowerCase() === 'none') {
throw new TypeError("invalid alg: none")
}
if (!(parser instanceof JwtParser)) {
throw new TypeError("invalid parser class")
}
this.parsers[alg] = parser
}
getParserForAlg(alg) {
if (!(alg in this.parsers)) {
throw new Error('no supported parser')
}
return this.parsers[alg]
}
gerParser(token) {
const header = getJwtHeader(token)
return this.getParserForAlg(header.alg)
}
decode(token) {
return this.getParser(token).decode(token)
}
}
This one trick of One Parser per Algorithm will avoid the most common footguns for JWT implementations.
JSON Web Encryption
Everything mentioned previously assumed JWT for signatures, because that is the most common use case for a JWT library.
If you need JWE (JSON Web Encryption), everything outlined previously also applies to handling the "enc" header, not just the "alg" header.
Your JWE parser should have one hard-coded "alg" type (with a corresponding set of allowed keys) and one hard-coded "enc" type. The actual content-encryption key is wrapped with the first key, so you don’t need any special considerations for the keys associated with "enc".
However, that does lead into my next topic:
JWT Key Management
If you peruse RFC 8725, you may notice an entry about Weak Symmetric Keys. Developers were shoving weak passwords in their source code and the JWT library just said, “Yep, this is fine.”

Key management in general is several orders of magnitude more difficult than you may believe, as Lea Kissner puts it:
Cryptography is a tool for turning a whole swathe of problems into key management problems. Key management problems are way harder than (virtually all) cryptographers think.
Lea Kissner
I won’t aspire to tackle the entire subject of key management (which would probably warrant a series of blog posts, rather than a section of only one, to cover adequately).
Instead, let’s talk about making our software misuse-resistant. This is best illustrated by example.
Case Study: Halite
Halite is a PHP library that wraps libsodium to provide encryption, authentication, etc.
Halite doesn’t let you pass an arbitrary string to a constructor or method that expects a cryptographic secret. Instead, it requires that you pass an instance of a Key class. These Key objects also cannot be human-memorable passwords, and no mechanism is defined to easily create an insecure key.
Halite’s key API looks instead like this:
<?php
use ParagonIE\Halite\KeyFactory;
$encKey = KeyFactory::generateEncryptionKey();
KeyFactory::save(
$encKey,
'/path/outside/webroot/encryption.key'
);
To then load this key:
<?php
use ParagonIE\Halite\KeyFactory;
$encryptionKey = KeyFactory::loadEncryptionKey(
'/path/outside/webroot/encryption.key'
);
At this point, you might be thinking, “Hah! I can just create an arbitrary file on disk and tell Halite to use that!” but nope! Halite keys have a specific format that includes a checksum to prevent this sort of cleverness.
It’s certainly possible to work around this limitation, but it’s far easier to use Halite the right way than the wrong way. Simple mechanisms work.
Misuse Resistant Keys for JWT
Aspirational JWT library developers can simply do what Halite did:
- Define a class of objects for cryptographic keys which only permits their usage for a single cryptographic algorithm.
- Even better: make a child class for each algorithm they’re intended for, and use type declarations to enforce this acceptance criteria. This will leverage many developers’ IDE autocomplete features and reduce friction for the correct usage.
- Do not allow users to type
new JwtKey("my password yolo")in any sense. This usage pattern is forbidden. - Provide an encoding/decoding method (that uses a checksum) for loading keys from persistent storage.
- Allow users to easily generate keys from a cryptographic random number generator, which can later be persisted.
I won’t include any example code for this section because the way your solution should look will be different for each programming language. Halite is a recommended example for PHP software.
Avoid Network Calls When Validating Keys
Put simply: If you can get away with forbidding jku and x5u from your library, do that, and call it a day.
There are many reasons to avoid these features. RFC 8725 discusses some of them. Additionally, if you’re ever faced with an attacker that can eavesdrop and tamper with TLS traffic (say, they have a compromised CA cert), they can feed whatever key they want into your system.
If you must support fetching public keys from a remote endpoint, consider an asynchronous automated process that caches the public key on your end after it’s first observed and requires it be checked into something like Certificate Transparency. See also: Chronicle. If nothing else, this gives you an append-only audit trail of all public keys seen.
For the developers in the audience, this can also be sold to project managers as a performance win: You’re avoiding a network call to validate tokens, which means validation can be much faster.
At minimum, the jku and x5u URLs should be checked against an allow-list. If it’s not found, fail closed (i.e. abort as an invalid token). Additionally, only asymmetric public keys should be accepted from any URL.
Use Opinionated Cryptography
There are a lot of ways you can use JWT. Not only should you ignore most algorithms (except the ones you actually need), you should ensure this list only includes algorithms that cryptographers approve.
For example, rather than implementing “is this point on the curve?” checks for ECDH-ES support for JSON Web Encryption with a bignum library and hoping for the best, only accept the X25519 algorithm in this context. You don’t have to worry about invalid curve attacks with X25519, because point compression is baked-in. If you cannot refuse to accept the NIST curves, then lobby the JOSE Working Group to specify point compression for JWT, then only support compressed points. The patent on point compression expired. Start using it everywhere!
Entire bug-classes can be killed by refusing to support dumb cryptographic choices.
Scott’s Cryptography Algorithm Recommendations
In this list, choose only one option from each category (if you even need that category at all), unless told otherwise by your system requirements. I will order them in terms of priority (best to least).
- JSON Web Signatures, Asymmetric
- JSON Web Signatures, Symmetric
HS384HS256
- JSON Web Encryption,
"alg", SymmetricA256GCMKW- Be mindful of the number of keys ever wrapped with symmetric algorithms. If it ever exceeds 2^31, rotate to a new key.
A128GCMKW
- JSON Web Encryption,
"alg", AsymmetricECDH-ESwithX25519RSA-OAEP-256
- JSON Web Encryption,
"enc"A256CBC-HS512A256GCM
A128CBC-HS256A128GCM
Do not implement support for RSA with PKCS1v1.5 padding (RS256, RS384, RS512, RSA1_5).
Do not implement support for direct use of a shared symmetric key for content encryption in JWE (dir).
Do not implement support for password-based encryption (PBES2), or you’ll open your system to trivial denial-of-service attacks (via the p2c value).
For encrypted tokens with asymmetric cryptography, you should also enforce a construction that both signs and encrypts. and don’t let your users opt out of it. The reason for this is simple: Asymmetric encryption generally doesn’t authenticate the sender. The Connect2id folks suggest signing then encrypting, for fear of signature-stripping attacks. I don’t have an opinion on this order, presently.
Other Considerations
If you’re going to encrypt anything, don’t allow data compression. Compression oracle attacks are fun for researchers, not so fun for defenders.
RFC 8725 also covers some non-cryptographic corner cases, like character set encoding choice, that can have security implications. Double-check that you’re adhering to their recommendations in addition to what I’ve outlined above.
If you’re using ECDSA signatures (ES384, ES256), be strict about them. Both (r, s) and (r, n – s), where n is the order of the curve, are solutions for the ECDSA curve equation. This means that signatures have an undesirable malleability that matters more to blockchains than security tokens. Even still, I recommend you reject (r, n – s). Many ECDSA libraries already offer this as an optional feature.
Closing Thoughts
The amount of legwork and careful engineering needed to hack the JWT standards into something that won’t fall over the moment someone clever looks at it is breathtaking.
Even if you succeed, you’re continuing to prop up a standard that many people get wrong, and likely will continue to get wrong, for the sake of interoperability. Consider PASETO instead.
By tackling this problem from better security fundamentals than the JWT designers used in 2015, you can arrive at a serviceable and robust library. However, constant diligence is needed to prevent feature creep from introducing a security vulnerability. Learn to say “No”.
At the very least, if you follow these recommendations and still get a cryptographic CVE in your code, it’s likely to be a much more interesting attack than the Usual Suspect.
3 responses to “How to Write a Secure JWT Library If You Absolutely Must”
What is the last line of this constructor trying to do? Is it intentional?
export class JwtParser {
constuctor(alg, keys = {}) {
this.sigAlg = getSignatureAlgorithm(alg)
this.keys = {}
for (let k of keys) {
this.addKey(k, keys[k])
}
this.keys = keys
}
LikeLike
Vestigial. I will correct.
LikeLike
[…] How to Write a Secure JWT Library If You Absolutely Must (du même auteur) […]
LikeLike