Yoong Kang Lim

Statelessness is not unique to JWTs

JSON Web Tokens (JWTs) are commonly used today in a number of applications, especially as bearer tokens.

JWTs are often said to have the advantage of being “stateless”.

By that, it means that it is possible to validate a token without checking any persistent store, like a database or something like Redis. For every authenticated request, we are able to avoid making an extra hit to the database just to validate the user session.

This statelessness is sometimes said to be the main purpose of using JWTs.

In reality, statelessness is a feature that is not unique to JWTs, and I would argue here that it is probably not the best reason to use JWTs if that is the only benefit you get out of it.

Let’s first examine what properties allow JWTs to be stateless, and then discuss if this is indeed the main advantage of JWTs.

JWTs and cryptographic signing

When people talk about “JWTs”, what they usually mean is a variant called JSON Web Signature, or JWS. Another variant is called JSON Web Encryption, which as far as I know is not as commonly used.

As its name implies, the JWS variant uses cryptographic signing. If you are new to cryptography, signing is a way to create something called a “signature”, a bit of data that is impossible to create without a secret key. By checking this signature, we can verify that it was generated by the party holding this secret key.

You can find examples of JWTs online, but a JWT’s structure is basically this:

header.payload.signature

There are three parts, separated by a period. The header and payload are both encoded with URL-safe base64 encoding, which means that anyone can see their contents by decoding them. The last part, the signature, is generated using cryptographic signing.

This signature cannot be generated without knowing the secret, which means that a server can be confident that it was generated by a party that knows this secret. If this was generated using symmetric cryptography, then the same key will be used for both signing and validating the signature. The JWT specification also allows for signing using asymmetric cryptography, which means that it is signed with a private key, and validated with a public key.

It also follows that, in order to validate the token, a server will only need to know the secret key (if using symmetric signing) or the public key (if using asymmetric signing). This means the server does not need to check a database to verify that it is a valid token, allowing JWTs to be “stateless”.

That sounds great, what’s the problem?

In reality, there is nothing about this that is unique to JWTs.

A token can be stateless without it having to be a JWT. It just needs cryptographic signing, the same way JWTs use cryptographic signing.

Essentially, all you need to do is use HMAC with SHA to generate a signature, and send it together with the data serialised in some way (e.g. URL-safe base64 encoding).

In fact, major web frameworks like Django allow you to use stateless tokens. If you configure Django to use cookie-based sessions, you are essentially using stateless signed tokens, only that the token is stored and transported in a cookie. That is why it also comes with a few problems of stateless tokens that JWTs also have, namely the problem of invalidating the tokens. No surprises that Django’s implementation uses HMAC with SHA.

By avoiding JWTs, you also avoid all the problems that come with parsing the header. The JWT header was the source of a number of security issues in the past, specifically that it allowed the client to specify the algorithm that the JWT is signed with, of which “none” is an option. Luckily, this has been fixed in most libraries, but that is still not a great sign.

In my experience, I found that there are other security footguns in the JWT/JOSE specification as well, especially those that come with asymmetric algorithms. For example there are things like the jku claim in the header, that tells you the URL of the public key you should use to validate the token. That is a strange thing to include in the token, because an attacker could generate a token and make the jku claim point to a public key that belongs to the attacker. If the developer naively trusts these built-in claims, then they make their systems vulnerable to these exploits. More information can be found here. The specification, in my opinion, has a number of things that require the developer to know about and guard against.

If the same server or party is both generating and also validating the token, it is probably best to avoid JWTs entirely. Using HMAC and SHA requires very rudimentary cryptography knowledge, has good library support (especially if you use a web framework like Django), and is generally not that easy to mess up.

On top of that, by using HMAC + SHA to generate a token, the token can still be stateless! So, you get all the benefits of statelessness, and none of the disadvantages.

So are JWTs useful at all?

Some would argue that JWTs are not useful, at all. Especially not for session tokens.

I would probably take a softer stance, and say that it can be a viable and pragmatic option in some cases. Note that I do not work in cybersecurity, and this opinion comes solely from the perspective of a developer who builds applications.

One scenario where I would probably use JWTs is if two different servers need to send data to each other, and these two servers are owned by separate organisations. This comes up more often than you would expect when you do software consulting.

For example, let’s say I am writing a Python app, and my client has a Java web service. For some reason we need to exchange some data, probably via the user’s browser or mobile app.

By the way, this is a situation similar to OAuth2, where an authorisation server generates a token to a user, who then sends it to a resource server that parses that token (OAuth2 uses JWTs).

If I use JWTs, I can be fairly confident that my client would be able to decode the JWT using a reasonably mature library.

Since JWT/JOSE is, for better or worse, an established standard by now, I could probably do a lot worse than making my client work with a well-tested JWT/JOSE library. The alternative is to agree with the client to create a token format using an ad hoc serialisation and signing scheme, which can take more development time (both for my client and for myself), and requires a bit of crypto knowledge.

There are more alternatives, of course, like PASETO which was created to address the deficiencies of the JOSE standard. I will not go into those alternatives here.

Can you show me some code to create a stateless token in Django?

Sure. Use the signing module.

Here’s how you do it:

from django.core import signing

data = {'user_id': 123456}

# Here's how you generate a token
token = signing.dumps(data)

# Here's how you validate the token
decoded_data = signing.loads(token, max_age=3600)

If you don’t use Django, find a trusted crypto or HMAC library in your language of choice. In Python you could just use the built-in hmac module. Make sure you compare the signature using a constant-time compare algorithm to prevent timing attacks.

Summary

Statelessness is often touted as an advantage of JWTs, but in reality that feature is not unique to JWTs. Any token that was generated using cryptographic signing can be stateless, including JWTs.

Furthermore, JWTs introduce some extra security burden on the developer, who needs to be extra careful to avoid the pitfalls of JWTs.

If the same party is generating and validating the token, in my opinion it is best to avoid JWTs entirely. A very good option is including a signature of the payload using HMAC + SHA. Django provides this out of the box, and the Python standard library gives you some good tools to use. This gives you stateless tokens without having to use JWTs.

In my personal opinion as a developer (and not a cybersecurity professional), JWTs can be a pragmatic option in some cases where a standard is useful, as this means decent library support which avoids extra development effort. One such case is when the token needs to be generated by one party, and validated by another.