Alternatives to JSON Web Tokens (JWTs)

Scott Brady
Scott Brady
JOSE

JSON Web Tokens (JWTs) get a lot of hate from the wider crypto community, but what are the alternatives? In this article, I am going to give a high-level overview of some of the recommended alternatives mentioned in Twitter rants and attempt to provide an opinion on whether or not they can replace JWTs.

I in no way want to become the defender of JWTs; this is not the hill I want to die on. However, with the increasing hate on JWTs and what I see as misunderstandings around them and their alternatives, I felt that I had to put something into writing to clear my head.

If you know of any other alternatives, let me know and I will see about adding it to the article.

Why the Hate on JWTs?

Before we look at alternatives, let’s take a quick look at the criticisms around JWTs.

Algorithm Selection (Ciphersuite Agility)

In their header, JWTs include an algorithm (alg) value that states how the token was signed or encrypted. This is a feature called ciphersuite agility. Taking the JWT specification at face value, this value could be changed to none (no signature), to a weaker algorithm, or even to a symmetric signature using an RSA public key.

In my opinion, this issue is mitigated when dealing with protocols such as OAuth and OpenID Connect, where JWTs excel and have further standardization. But, unfortunately, this issue has caused too many vulnerabilities to ignore.

To read more about this criticism, have a read of the popular blog post “No Way, JOSE!”.

Misuse of JWTs

JWTs are not suitable or designed for stateless sessions or storage. This one seems to be prevalent in Single Page Apps (SPAs), and it strikes me as a way of avoiding server-side code or protocols such as OAuth and OpenID Connect.

I won’t continue talking about this issue because I don’t think talking about the weird and wonderful ways a JSON construct can be misused will help. None of the following alternatives address this misuse and nor should they.

“Developers should not be trusted with security”

A toxic sentiment that seems to affect all communities: “x shouldn’t be trusted with y”, where y is a thing that I do for a living. Sure, we’ve seen some crazy security decisions from software developers, but we’ve seen the same mistakes and crazy code from IT pros, mathematicians, and cryptographers.

It’s also strange that the crypto community criticizes the spec writers for being developers, but developers criticize the spec writers for not being developers. It seems the specification authors cannot win.

Finger-pointing, patronizing, and shaming other communities does not help anyone. I’ve been upset enough to write about this attitude before, so let’s leave this criticism here.

JWT Alternatives

When looking at alternatives for JWTs, I’ll be judging them by the following categories:

  • Does it support public-key cryptography? This would allow it to replace the typical use case of JWS in a zero-trust system
  • Does it support encryption with asymmetric keys? This would allow it to replace JWE
  • Has it been standardized with a standards body? For instance, the IETF or even a private institution such as the OpenID Foundation or FIDO Alliance
  • Can it enforce a ciphersuite suitable for 2020?
  • Do its implementations offer standard claim checks such as issuer, audience, and expiry? Any JWT library worth its salt provides this out of the box, whether it is in the spec or not.

Fernet

Fernet is a custom token format that uses AES 128-CBC for encryption and HMAC-SHA256 for authentication (encrypt-then-MAC). As a result, it needs both a signing key and an encryption key for token generation and token validation.

A Fernet token is the concatenation of the following values, base64url encoded:

Version (1B) | Timestamp (8B) | IV (16B) | Ciphertext (*B) | HMAC (32B)

Where HMAC is generated from the concatenation of the previous four values.

Therefore, a token containing a JSON payload of { "sub": "123" }, would just look like a single base64url encoded value:

gAAAAABenGL6_-7RHv3KDSZsvORpX3CEM37uS0ABxhfQn47dxzlk4TKMB6JBnxgodSGL6r-gi6rf8u8IZ1I3mZSgu9kxyx2-XJLdRL5O7sGPEnQllTDGQ5I

Fernet vs. JWT

Public-key Crypto Encrypted by Default Standardized Modern Crypto Standard Claim Checks

Fernet meets some JWT use cases. It uses symmetric encryption (a shared key), so that means it would replace a JWE, where both parties can both encrypt and decrypt. In a private/internal system, Fernet might work for you; but in a zero-trust system, this would not be suitable.

At the spec level, Fernet only supports AES-128-CBC, which is a little old hat now. You could consider creating your own version, which uses your own ciphersuite; after all, it would only be used in a private system. However, now you can no longer definitely say “we use Fernet”, and have people just understand what you mean. It also means that Fernet library builders have to open up their libraries to allow custom versioning and algorithm choice, and that feels like a slippery slope that almost brings us back to “ciphersuite agility”.

The payload in a Fernet token can be anything you want. Most implementations treat it as a string.

Opinion

Fernet itself is something of an unknown. It seems to have been extracted from a Ruby library developed at Heroku. Fernet is also an Italian liquor, which makes it very hard to Google. The GitHub spec was created in 2013 and hasn’t really been modified since. The question “Why Fernet?” was posed in 2016, but it hasn’t received a response from the project owners.

In my programming language of choice, there is 1 implementation of Fernet with 5 GitHub stars. However, the reference implementations in Ruby and Go seem to be receiving maintenance updates.

From what I can see, Branca is the continuation of the Fernet project with active authors. I’m confused as to why I’m seeing recommendations for Fernet in 2020.

Branca

Public-key Crypto Encrypted by Default Standardized Modern Crypto Standard Claim Checks

Branca is a modernized version of Fernet; hence the name (Fernet-Branca is a popular brand of Fernet). Instead of AES-128 and an HMAC, it uses XChaCha20-Poly1305 for authenticated encryption. The other main differences are the use of base62 encoding, rather than base64url encoding, and a Unix epoch timestamp.

Version (1B) | Timestamp (4B) | Nonce (24B) | Ciphertext (*B) | Tag (16B)

Therefore, a token containing a JSON payload of { "sub": "123" }, would look like the following base62 encoded value:

5K6OibgVLljjGms4Ttpywj3zB87mANBckFjG0JQ32w94rAh48dTDiuQezoCqcBKTiH2tIGNvBF53lYoPbW9odJo1K2u713kyVo9Wbx5PiFPQ2SGyz7N1xXQTsuGYmNR4mvZA2RlxNAaEHxCPbN0DbI8

Branca vs. JWT

Branca is definitely more up to date than Fernet, but it still falls into the same category of use cases, where it is only suitable for internal, high-trust systems.

Branca uses an up to date encryption algorithm, and the spec has commits from 2020, which suggests that a new version could be created if there is ever the need. This is better than JWE, where the choice of encryption algorithms could cause a developer to shoot themselves in the foot.

The payload in a Branca token can be anything you want. Most implementations treat it as a string.

Opinion

I quite like Branca, and I really appreciate the pragmatic approach that the author takes when describing how JWTs can be used with Branca (there a couple of misconceptions around JWTs in the article, but it is a decent bit of writing).

The out of the ordinary encoding does confuse me, though. Base62 may be reasonable to some, but this is the first time I have encountered it. I also struggled to find support for this encoding when implementing Branca or a reason to use it over the usual base64url encoding.

In my chosen programming language, there is 1 implementation of Branca with 4 stars on GitHub. It has not been released.

I’ve since created my own implementation of Branca in .NET with JWT payload validation rules enabled by default.

Platform-Agnostic Security Tokens (PASETO)

PASETO was created in direct response to its author’s concerns with JWTs and the various JOSE specifications. JWTs and PASETOs are conceptually the same; however, PASETO removes the ciphersuite agility (pick your own algorithm) and enforces versioning.

A PASETO contains a version, a purpose, a payload, and an optional footer. The payload is a concatenation of the message and the signature .

version.purpose.payload.footer

A token containing a JSON payload of {"sub":"123","exp":"2020-04-19T19:39:14.054394Z"} would look like the following:

v2.public.eyJzdWIiOiIxMjMiLCJleHAiOiIyMDIwLTA0LTE5VDE5OjM5OjE0LjA1NDM5NFoifXN3tFWyv4sIenMsDkJyjTfdf8LWHYGA_mNSESIF5g8idp52ajbFZNQ2FNe_jDCGEAwH79xt9skelN_25yaFTAM

The version sets the token to compatibility mode (v1) or recommended mode (v2) and dictates what algorithms are used. More versions can be added in the future.

PASETOs come in two flavors: public and local. Local tokens are encrypted using a shared-key (similar to Fernet and Branca), while public tokens are plaintext but signed using digital signatures (similar to your traditional JWS).

Combining the version and the purpose tells you which algorithms to use. You’ll see this list everywhere, but PASETO use the following algorithms:

  • v1.local = AES-256-CTR + HMAC-SHA384 (encrypt-then-MAC)
  • v2.local = XChaCha20-Poly1305 (the same as Branca)
  • v1.public = RSASSA-PSS using SHA-384
  • v2.public = Ed25519

Unfortunately, PASETO is not pronounced pacito as in despacito, but rather “Paw Set Oh” ☹

PASETO vs. JWT

Public-key Crypto Encrypted by Default Standardized Modern Crypto Standard Claim Checks
(local)

Unlike Fernet and Branca, PASETO is suitable to replace both JWS and JWE.

Versioning brings the idea of unambiguous cipher suites. You see that it is version 1, and you know that it could only ever be signed using RSA-PSS. This means that implementors know precisely what they are getting and that they don’t necessarily need to understand the best algorithm to use or understand the history of JWTs to avoid any gotchas.

However, PASETO does not offer any protection against type confusion between symmetric keys and asymmetric keys. Instead, it relies upon the library implementor to protect against this, just as JWTs do. Without the library implementing this protection, PASETO can fall victim to the same attacks as JWTs. If we look back at the JWT criticisms around algorithm selection, PASETO’s versioning only removes the use of the none algorithm.

PASETO is designed as a replacement for JWTs, particularly their use in protocols such as OAuth and OpenID Connect. Implementations mostly remember this during token generation with helper methods such as AddIssuer, but in my experience, they regrettably seem to forget to use this during token validation. For instance, methods such as validate or decode only check the signature and not standard claims such as issuer, audience, and sometimes even omit expiry.

Opinion

PASETO fixes the concerns around JOSE’s ciphersuite agility, but, in my opinion, it doesn’t do itself any favors by phrasing itself as a replacement.

In my experience, I have seen more vulnerabilities caused by JWTs due to incorrect token payload validation, rather than ciphersuite agility. Whether it is insufficient issuer and audience validation or simply confusion caused by inadequate library design (e.g. decode vs. validate).

By acting as a replacement, new libraries are being created that fall into all the same traps JWT libraries did. Developers are being given a different foot gun, and I fear we would have an implementation wide regression on our hands.

The author of PASETO made an excellent point when they said that the quality of the library should not matter; the implementation should not have to protect you from the issues in the spec. Unfortunately, by not making claim validation a requirement (only strongly recommended), I feel that this JWT issue was not addressed with PASETO.

I’m also concerned by the attempt to standardize PASETO. I see articles stating that PASETO is currently a draft RFC with IETF, but unfortunately that is not true. The author did submit their initial write up of PASETO to the JOSE working group but seemingly abandoned it after some initial questioning from the working group.

Unfortunately, the draft has expired, it was never formally adopted by the working group, nor is anyone currently championing it. The spec hosted on GitHub has not been updated by the author in 2 years. Instead, PASETO continues to be its own thing, and JOSE continues to be criticized by a community that is not talking to the standards bodies.

In my chosen programming language, there are 2 implementations for PASETO, with an average of 23 stars on GitHub. I would not consider either of them actively maintained.

I’ve since created my own implementation of PASETO in .NET with JWT payload validation rules enabled by default.

“Just sign some JSON”

Sometimes, when challenged for an alternative to JWTs, you’ll see people just list off various “plain” signing algorithms or cryptography libraries such as Google’s Tink. The argument is to just do it yourself instead of using an abstraction that gives you the possibility of getting it wrong.

Sure, that could work, but it is a different use case, and I feel this sentiment misses the point of JWTs. You’re going way off-spec with this approach, you lose your standardization, and now you have an orchestration problem on your hands.

To be as flippant as the Twitter arguments: if this seems like an alternative to JWTs to you, then you were probably using JWTs wrong.

How Can We Replace JWTs?

Well, I don’t think you need to. Instead of replacing JWTs, we need to build upon them with the features found in alternatives such as PASETO and Branca. No matter your opinion of JWTs, their issues are solvable.

Public-key Crypto Encrypted by Default Standardized Modern Crypto Standard Claim Checks
Fernet
Branca
PASETO (local)
JWT (JWE)

If we take a look at all of the alternatives, we can see that PASETO is the only option that comes close to replacing all use cases of JWTs. But, as I’ve discussed, I have reservations about using PASETO and outright replacing JWTs.

I can see how using a concise solution like Branca would dissuade developers from using JWTs with wrong symmetric algorithms; however, with both PASETO and Branca, I am still concerned about the lack of specification around the payload validation process.

From the available alternatives, there doesn’t seem to be an option with enough traction to start competing with JWTs any time soon. They are still too niche.

Versioned JWTs

When PASETO was presented to the JOSE working group, an excellent suggestion was made by Neil Madden:

You could write an RFC that proposes 4 new JOSE algorithms: “v1.local”, “v1.public”, “v2.local”, “v2.public” and marks all others (and associated headers) as prohibited. Wouldn’t that be pretty similar to the core of PASETO? The fundamental problem with “alg” in JOSE is that it requires you to trust the message to tell you what algorithm to use to validate that message. PASETO makes exactly the same mistake, it just has a much better (and much smaller) algorithm selection.

I think the work you are doing on PASETO is valuable and has a lot of nice ideas. I would also say that is already better than JOSE as it currently is, but I’m not sure it is the best replacement.

- Neil Madden, JOSE working group mailing list.

This is a no-brainer to me. It adds the core feature of PASETO to JWTs, solving the most significant criticism with JOSE without the need for breaking changes across the internet or the possibility of regression.

The author of PASETO seemed to agree with this suggestion but unfortunately never pursued it and continues to push PASETO as a replacement. I’m unsure why.

Combining a versioned JWT with mandatory claim validation sounds like a robust solution to me, and I would love to see this idea explored further. Or, maybe we should remove all references to algorithms and versions from the token altogether.