It's a JWT and it's pronounced jot

Posted on Jan 18, 2023

We're talking security, so here is a wall

If there is one thing I like about computing, it is the arguments: framework X vs Y, language A vs B, Mac vs Linux vs Win$, Arch Linux vs the world, sysadmin vs the rest of the team.

The problem with arguments is that they can generate noise and hide the useful information. That is exactly what I ran into while implementing JWT on an internal project at Coddity, and on one specific case: user session management.

What I like about this topic is that it is a real engineering question, with consequences for security, the complexity of a system, and data integrity.

Between the todo-app tutorials and the more serious articles that turn into ego fights in the comments, it gets hard to form a clear picture of how JWT should be used, and of what you actually have to implement for this use case. And spoiler alert: at a minimum it is not simple, and it is not the best idea in the world.

The point of this article is therefore to synthesize what JWT is, what it allows, what it does not allow, and the risks it introduces, in the case of user session management.

Hashtag-wise, we are going to talk about: #standard, #encryption, #stateless, #revocation, #none, #XSS, #CSRF, #cookie, #DB

DISCLAIMER1: Article written by a human, with no help from ChatGPT. DISCLAIMER2: Illustrations by the author

This meme again???

JWT, and it’s pronounced [jot]

JWT is defined in the following RFCs:

  • RFC 7519, from 2015, specifying the format
  • RFC 7797, specifying the options for signing a JWT: JWS
  • RFC 8725, more recent (2020), covering implementation best practices.

Already, the fact that JWT gets a follow-up RFC five years later, dedicated to best practices, should logically raise an eyebrow.

What a JWT is for

What does the RFC say?

JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted.

JWT is a mechanism for exchanging data, the “claims”, between two systems. Those claims are encoded in JSON and signed. The JWT is generated by the application server, which signs it with a key, so that the server can later verify the authenticity of the data when it exchanges messages with the client. Because the claims inside the JWT are signed and not encrypted, the data exchanged in a JWT is in the clear, merely base64-encoded.

Structure

A JWT is a sequence made of several parts separated by a ‘.’. Each part is a component of the JWT, base64-encoded:

  • header: containing the type and the algorithm used to sign
  • payload: containing the claims, that is, the information we are carrying
  • signature: the signature of the header and of the payload according to the signing algorithm

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.YEAWENac8QpCBNlp1XVecfh8CY1DGHfO_vnrsiA7B20

maps to the following structure:

  • Header:
{
  "alg": "HS256",
  "typ": "JWT"
}
  • payload:
{
  "sub": "1234567890",
  "name": "Debbie Harry",
  "iat": 1516239022
}
  • signature: HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload)) with a secret key “un super secret”

The header holds the information needed to verify the signature of the JWT, in particular the algorithm used. I want to draw your attention to this: it is the JWT itself that tells the server how to check its own integrity. We will come back to that below.

Claims

Claims are the data exchanged inside the JWT. Some of them are expected in the JWT so that the server does not reject it. And nothing stops you from putting the lyrics of “Blitzkrieg Bop” by the Ramones in the JWT. You can create your own claims (there is in fact a limitation, and a big one, which we will see below).

The main standardized claims are:

  • “iss” for issuer: identifies the system that issues the JWT (optional)
  • “sub” for subject: identifies the subject of the JWT (optional)
  • “aud” for audience: identifies the system the JWT is intended for (optional)
  • “exp” for expiration time: obvious (optional)
  • “nbf” for not before: identifies the time before which the JWT must not be accepted (optional)
  • “iat” for issued at: obvious (optional)
  • “jti” for JWT ID: unique identifier of the JWT (optional)

You will notice that all of these claims are optional according to the RFC. WAIT FOR IT: we will see later that these elements matter, and that they are potential security holes if the claims are misused.

And as said earlier, nothing stops you from having a custom claim that contains your entire representation of a user.

  • payload:
{
  "sub": "1234567890",
  "iat": 1516239022,
  "user":{
    "userID":"id",
    "firstName":"Debbie",
    "lastName":"Harry",
    "address":"1st street, NYC, USA",
    "employer": "Blondie",
    "bankAccount":"US3723000144 0049 123123"
  }
}

Signature

The signature will be…(drum roll)…the signature of the header and of the payload, according to the signing algorithm specified in the header.

Debugger

You can use this debugger to test your JWTs.

Advantages

Once these points are granted, the advantages of JWT appear:

  • The JWT contains data the server can access without going through a database: you can pass, as claims, whatever information about the user you want. No need to fetch it with a database query (wait for it)
  • The signature of the JWT guarantees the integrity of the data that was transmitted
  • No need to store a session token in the DB. Once the server has generated it, the JWT is stored on the client side. JWT therefore allows stateless authorization of requests
  • It can be shared easily between different services
  • A JWT is easy to use in an environment that scales

User session

Stateful vs Stateless

A user session is a way to create state (stateful) for a client using a service, inside a data-exchange system that is mostly without state (stateless).

The simplest use case is a user who logs in to an application by providing their credentials and who, on every request between the client and the server, gives the server a way to identify and authorize that user.

session cookie

When the session is created, at login, the server can:

  • bind it to a single user
  • attach permissions to it
  • attach a lifetime to it
  • attach data to it

The server creates a session ID and does not have to send constructed data to the client.

Session lifecycle

A session can come to an end in several cases:

  • the user logs out
  • the session expires naturally
  • the server invalidates the session, for security reasons for example
  • the server bans the user

In every case, the server only has to delete the session in the DB for that end to take effect.

Persistence

On every user request, a connection to the database that stores the session is required in order to authorize it. And that last point raises a problem: connecting to a DB on every request is expensive in terms of performance. You could store the sessions in the server’s memory, but that de facto prevents the system from scaling. However, using an in-memory database to store the sessions can limit the performance drop on each request.

On the client side, the user session is traditionally persisted with session cookies which, depending on the implementation, can open security holes. We will talk about that a bit further down.

Creating a session with a JWT

the server generating the JWT

“But wait, if I followed correctly, we can use the JWT since it is stateless, and all we have to do is check its signature to validate it! That way we get rid of the DB, it’s faster and more secure! YOLO” Boris, young developer

request validation

Pow Pow Pow, settle down

Is JWT for managing user sessions really Stateless?

Yes, the JWT is stateless. A user logs in, the server generates a JWT with the user’s information, and it is sent back to the client, which can send it again in its exchanges with the server.

Except… You can see the problem coming.

Because there is no persistence of the JWT on the server side, the server can only check its signature, check its “validity date” and, if the claims were created correctly, whether the user can access the requested resource.

On the other hand: how do you log a user out? how do you ban a user? There is no way to invalidate a JWT, and until its expiration date the user will be able to access the requested resource.

“OK, in that case we add a DB of banned or logged-out JWTs and we’re done, right? Or better, we change the signing key?” Still Boris

Indeed, you can add a database, a fast one, in-memory, to store the JWTs the server must treat as invalid. And… you end up with the same architecture as session cookies. And changing the key? Every JWT belonging to every user gets invalidated. A solution to throw out as soon as you go past 2 users. Another idea would be to give them very short lifetimes, but that creates a usability problem. A user who lands on a login prompt every 2 minutes is not going to enjoy it. You could use a refresh token, but you are back to square one, because that refresh token has to be invalidatable, which means a DB, and so on.

A question of weight

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySUQiOiJzM2c0NTYzMXFzZzEzcXNkZmciLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTUxNjIzOTIwMn0.lMX8ytjSqvingScbR3IQrbZ3aESaSeQuhfg2nsvUXFQ which corresponds to the following claims:

{
  "userID": "s3g45631qsg13qsdfg",
  "iat": 1516239022,
  "exp": 1516239202
}

These claims are the strict minimum we can implement: a userID that identifies the user, an expiration date, an issued-at date, the whole thing coming to 168 bytes. What would be the size of an equivalent signed session cookie? A few dozen bytes, so at least a factor of 5. All the more so because, with these basic claims, as we will see below, we do not have a secure solution. We will have to add keys. Imagine you want to add claims related to the user (nothing stops you, and it avoids calls to the DB): the size of the JWT will inexorably grow.

But why is that a problem?

Besides the increase in bandwidth required, how do you transmit the JWT? As we will see below, the most secure solution is to transmit it in an HTTPOnly cookie. But careful: the maximum cookie size allowed for a domain is 4kb in browsers, a size you can hit quickly if you set other cookies on the client. The weight of the JWT can therefore push you to transmit it in a header, and to store it in the browser’s local storage: security alert.

Client-side persistence

The golden rule:

“never trust information from the client” random senior dev

Suppose that, for some reason X, the JWT is transmitted in a header and stored in the browser’s local storage. Your JWT is vulnerable to XSS attacks, that is, an injection of malicious JavaScript into your client code. The solution is to pass it in a cookie, and, so that the cookie is not accessible from JS and an XSS attack is blocked, you have to set it as an HTTPOnly cookie. But remember, the JWT is signed and not encrypted. You can read the JWT’s data and its claims in the browser’s developer tools, for example. It is therefore preferable to sign and encrypt the cookie. (You can also encrypt the contents of the JWT payload, for the hell of it.)

Which still leaves us with several cryptographic operations, hungry for CPU and for processing time, on every request:

  • signing the JWT when it is created
  • signing the HTTPOnly cookie
  • encrypting the HTTPOnly cookie
  • decrypting the cookie
  • verifying the signature of the cookie
  • verifying the signature of the JWT

Had you used a session-cookie solution from the start, you would have saved, de facto, all the cryptographic operations tied to the JWT, which are unnecessary here.

The problem with HTTPOnly cookies is that you are open to CSRF attacks! You end up having to build extra protection into your client-server exchange mechanisms (but I am stretching the point: this protection is indispensable as soon as you handle user authentication with cookies).

Summary of the architecture

=> To manage the lifecycle of a user session, you have to use a database that will be consulted on every request.

=> To be able to use the JWT in a fully secure way, you have to transmit them via HTTPOnly cookies, which themselves require an extra encryption step.

=> To be able to use a JWT inside a cookie, you may have to limit the information carried in the claims.

Using JWT to manage a user session makes us lose all of its advantages: stateless, transport of useful information, speed. We will see a bit further down the case where, on the contrary, it is very interesting.

Implementation vulnerabilities

In the introduction I mentioned an RFC written 4 years after the initial JWT RFC, on best practices for putting JWT in place, addressed both to the authors of JWT libraries and to the users of those libraries. If this new RFC exists, there are good reasons.

I am deliberately leaving aside the risks around generating a JWT, or verifying it, inside the libraries. It is on you, and on the industry, to identify the ones that implement the RFC correctly.

Encryption

JWT includes, in its definition, the possibility of using a rather particular algorithm, the none algorithm, meaning no cryptographic algorithm at all, which makes it possible not to sign the JWT. Strange, isn’t it, for an authentication token? It was implemented for the cases where the integrity of the token is already protected by encryption at a transport layer, for example TLS.

The problem is that some libraries implementing JWT consider, or used to consider, a JWT signed with the none algorithm as perfectly valid, and will not reject the request! How is that possible?

Remember that the JWT header contains an “alg” field. That field tells the server which kind of algorithm to use to verify the signature.

Wait a minute. To validate the payload, you have to use a piece of information supplied by the client, an untrusted source?

An attacker can therefore intercept the JWT and send it back after modifying this field, using it as an attack vector, as in the case of the none algorithm.

To counter the none algorithm problem, the simple solution is to refuse any use of it if a key was provided in the implementation.

Other flaws exist around the choice of cryptographic algorithms. I refer you to this article and this one for more detail on the vulnerabilities depending on the algorithms used, as well as RFC 8725 mentioned at the start of the article.

The claims

Besides the ability to send custom data, claims provide control mechanisms that are interesting and relevant:

  • who issued the JWT
  • what the identifier of the JWT is
  • what the subject of the JWT is
  • which system is supposed to use the JWT
  • how long the JWT is valid

The problem is that these claims are optional, and nothing prevents leaving them unused in the implementation design, which is a risk.

So when are JWTs a good fit?

I think I have covered a large part of the reasons why using JWT to manage user sessions is a bad fit. They are not without interest, though, particularly for use cases that need a single-use authentication token.

For example, a client needs to access a resource sitting in an object storage (B). Your application server (A) can generate a JWT that, a fortiori, has a short lifetime, with a limited scope, so the client can fetch that resource. The limited scope here will be: access one specific resource on one specific domain.

Because they are stateless by definition, and because it is easy to put limited perimeters into them, JWTs are also interesting for server-to-server exchanges, or for exchanges between microservices.

Conclusion

In writing this article, I wanted to save you time, by letting you get past the famous “todo-app with JWT” articles you can find by the dozen on dev.to.

No, JWT is not natively suited to managing user sessions. It demands extra engineering that brings it close to the user-session systems that already exist. Besides, if you look at the authentication systems implemented in the most common frameworks:

  • symfony: session cookie
  • laravel: session cookie
  • django: session cookie

I wanted to bring in some Golang code to back the article up, but I think it could have lost the people who do not know the language. So, coming up: an article about implementing an authentication system in Go!

If you have comments on what is written above, feel free to send them to me by email at matthieu [at] coddity.com

POST SCRIPTUM: For the hell of it, I asked ChatGPT to give me the advantages of JWT. By now you are steady enough on your feet to notice that the “todo-app with JWT” articles are part of the model’s training data.

Hey ChatGPT, why is JWT a good idea?