Joshua's Docs - JWT (aka JSON Web Tokens) Cheat Sheet

Resources

What Type Link
Wikipedia page - surprisingly dev focused Wiki Wiki En
RFC spec from IETF Spec RFC #7519
JWT Debugger, Libraries, & More Playground & Quick Ref jwt.io

Intro - What is a JWT?

JSON Web Tokens, or JWTs, are a 'stateless' (more on this later) approach to authentication and session management, where the information about the auth or session (is user admin? Permissions? Etc.) can be stored within the token itself, rather than on the server.

Essentially, you can think of JWT as a container full of truths that the server has sent (these are called claims in JWT terms), and metadata about how these "truths" are stored (the algorithm and type).

In the most practical terms, a JWT can be represented as a single string, which you will see later.

Stateless? What does that mean?

JWTs are not always part of a stateless system, but the way they are composed lends them to that sort of setup.

With a typical non-JWT system, if you want to handle authentication and different permissions for different people, you usually use a combination of sessions and server-side storage - this would be your state. When a user logs in, you create a new session for them, with a unique ID, and to check permissions, you lookup their user ID against a permissions table and/or roles table. Each page that you load might require multiple DB lookups, to check for a valid session, then get roles, then get permissions, etc.

By contrast, with JWT, all the information about both the validity of the user (being logged in) and their role and/or permissions, is contained directly within the JWT string itself. The server, upon receiving an incoming JWT, simply has to verify that it is valid, but doesn't necessarily have to do any database lookups. This can make it stateless.

There are downsides to the this approach however, which will be discussed later.

Breaking it down further - the parts of a JWT

JWTs can seem overwhelming, so let's break them down part by part:

What do they actually look like?

First, you might be wondering what a JWT actually looks like. Well, the final string that is sent from the server to the client, to be stored in localStorage, sessionStorage, or a cookie, can look like this (real example):

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiQm9iYnkgVGFibGVzIiwiaWF0IjoxNTE2MjM5MDIyLCJpc0FkbWluIjp0cnVlLCJwZXJtaXNzaW9ucyI6eyJ1c2Vyc01ha2UiOnRydWUsInVzZXJzQmFuIjp0cnVlLCJ1c2Vyc0RlbGV0ZSI6ZmFsc2V9fQ.HFRcI4qU2lyvDnXhO-cSTkhvhrTCyXv6f6wXSJKGbFk

Enlightening, huh? Well, it's a lot less scary once you find out this is all it is:

base64Url(header) + '.' + base64Url(payload) + '.' + base64Url(signature)

Let's break it down further...

The pieces

As you saw above, the JWT is made up of three major components, each of which is base64 encoded (with the web-safe variant, to avoid characters that could screw up a URL) before being joined together. Each of these components, except the signature, is also a JSON object before encoding, but more on that below:

The header is the part that contains the metadata about the JWT itself. At the bare minimum, this will contain the type of algorithm used to encrypt the signature, but sometimes also contains extra metadata, like the type of token itself (JWT).

Example:

{
	"alg": "HS256",
	"typ": "JWT"
}

In this example, the header is saying "Hey, I'm a token of type JWT, with an encryption algorithm of HS256!"

Payload:

The payload is really the most important part in terms of functionality, not security. The payload contains the claims (or truths as I previously called them), that the server is saying belong to whomever holds the JWT and can send it back to the server.

When the user sends the JWT back to the server, via a header, URL, cookie, etc., the server decodes the payload and can use it in thousands of ways; for example, by allowing the user to make a "delete" action because one of the claims is that they are an admin.

Example payload:

{
	"name": "Bobby Tables",
	"iat": 1516239022,
	"isAdmin": true,
	"permissions": {
		 "usersMake": true,
		 "usersBan": true,
		 "usersDelete": false
	}
}

There are no mandatory key-pairs that your payload must have, although there are some that are standardized and common. For example, iat is a registered claim for issued at, and if included, must be a number representing the timestamp the JWT was issued. You should avoid conflicting with reserved/registered claim names. You can read more about them in the original spec, here.

Reminder: You should try to keep both keys and values short, since JWT is designed to be small.

Signature:

The signature of a JWT is the most important part when it comes to security. Essentially it is the value of the header + payload, put through a one-way encryption hashing function that uses a secret that ONLY the server or other trusted entities know.

If you base64 decode the value, it looks like gibberish, because it is a secure hash:

.T\#..Ú\¯.uá;ç.NHo.´ÂÉ{ú.¬.H..lY

The pseudo code to generate this hash, on the server, looks something like this:

HMACSHA256(
	base64Url(header) + "." +
	base64Url(payload),
	SECRET_KEY
);

The signature is a way for the server to, using its secret key**, validate that the JWT a user sends it is both valid, and created by itself or trusted creator.

(PS: The secret key I used throughout my examples here was krusty-krab-krabby-patty-secret-formula).

** = If using asymmetric encryption, you can use public key to validate. See "Signing Algorithms" subsection under "How is it secure?" section.

Breakdown summary - putting it back together

So, to summarize and show how these parts fit together once again, we are taking:

const header = {
	"alg": "HS256",
	"typ": "JWT"
};

const payload = {
	"name": "Bobby Tables",
	"iat": 1516239022,
	"isAdmin": true,
	"permissions": {
		 "usersMake": true,
		 "usersBan": true,
		 "usersDelete": false
	}
};

...making a signature by using a one-way encryption algo...

// Pseudo code
const signature = hashHS256(header, payload).withSecret(SECRET_KEY);

...and finally joining the parts together into a single string, after base64'ing each part:

// Pseudo code
const jwtString = base64Url(header) + '.' + base64Url(payload) + '.' + base64Url(signature);

How is it secure?

The security mechanism of JWT rests in its signature component (outlined above). To break it down further, when a user sends a JWT back to the server, the way the server checks to make sure that the claims should be trusted, is by validating the signature through:

  1. Take incoming header and payload
  2. Add SECRET_KEY that only the server and validator(s) know**
  3. Put header, payload, and secret, through one-way encryption hash
    • This creates a server-generated signature
  4. Check that the newly created signature matches the one sent in the JWT by the user
  5. If they match, that proves that the JWT the user sent was indeed created with the same secret owned by the server!

Please note that the strength of the security is strongly linked to your secret key - use a suitably long key (to avoid brute force attempts) that is never shared.

Side note: In addition to validating signatures, servers often also validate JWTs by checking that they conform to the expected structure and standards. See this auth0 guide for details.

** = or public key, if using asymmetric encryption, see below.

Signing Algorithms

In these examples, I'm using HMAC, which is a symmetric algorithm, meaning that there is only one private key and no public key. In order for more than one party to be able to create or validate a JWT, they must have the secret key.

With an asymmetric algorithm, a secret key is still used to create tokens, but a public key can also be used to confirm validity. This means that another server could create the JWT, and your server could still validate it by checking against the public key, without needing to share the private/secret key. This method is often preferred because it is a way for multiple parties to be able to validate, while only those with the private key can actually create tokens.

I won't get any further into the details here, since this is supposed to be an intro, but you can read more here.

Downside to JWT: Logging users out / invalidating tokens

So far, JWTs seem like they have numerous benefits over stateful authentication methods, but something we have not yet discussed is logging users out, deleting users, or otherwise invalidating or revoking tokens.

The truth is, this is where JTWs fall short. In order to invalidate a JWT, you need to have some sort of database / stateful system, because what you end up doing is maintaining either a blocklist or an allowlist.

With a blocklist, whenever you want to invalidate a token, you would add it to the blocklist table, and whenever a user tries to use a JWT, you would always need to check that it is not on the blocklist.

With an allowlist, it is basically the same thing, but add every token to the allowlist as it is created, and remove it from the allowlist when invalidating. And every incoming JWT would only be accepted if it is on the allowlist.

If you have to use either of these approaches, in my opinion that is symptomatic of your needs not aligning with what JWTs can provide, and it might be time to rethink your websites architecture to see if there is not a better alternative.

Avoiding state: Auto-expiring sessions

A pretty common workaround for sites avoiding having to implement a stateful JWT system for logging out users is to just auto-expire tokens quickly.

For example, you could either put the creation time of the JWT inside of its own payload, and/or the planned expiration time. Then, when validating incoming JWTs, simply check if the expiration time is earlier or equal to the current time - if so, reject the JWT.

The problem with this is that it is still a bit of a stop-gap solution and can end up being more complicated than just using sessions in the first place. In order to not have users getting constantly logged out while actively using the site (annoying!), you would need to use auto-refreshing JWTs, probably through a service like Auth0's Refresh Tokens. This still doesn't allow for a manual log-out flow, unless you also introduce revoking as part of the refresh pattern.

Avoiding state: Lazy invalidation

Imagine that you need to substantially upgrade your user management system, and force everyone to switch. A workaround that is unfortunately suggested by a lot of devs online, but is less than ideal, is to... change your private key secret. If you do that, you immediately invalidate all JWTs in circulation and force everyone to login again, since the signature portion of everyones' JWTs will no longer match.

Note on complexity, vulnerabilities, and more:

There is still lots to JWT that I have not covered fully in this post (it's supposed to be an intro, in my excuse). The reality is that many developers use a third party service (aka a Federated Identity Management service), such as Auth0, to manage the majority of the JWT stack. This won't magically solve many of the issues I've outlined with JWT, but it will make working with them easier and abstract some of the complexity and security concerns.

JWT Inspection Snippet

myJWTString.split('.').slice(0,2).map(atob).map(JSON.parse)
Markdown Source Last Updated:
Mon Mar 07 2022 03:11:28 GMT+0000 (Coordinated Universal Time)
Markdown Source Created:
Fri Sep 27 2019 11:10:32 GMT+0000 (Coordinated Universal Time)
© 2024 Joshua Tzucker, Built with Gatsby
Feedback