If you have been put off learning about JWT (JSON Web Tokens) because they sound scary or complicated, I have good news; they really aren't. In this short simplistic guide, I'll walk through the basics of JWT, what makes them unique, and their strengths and shortcomings compared to other forms of authentication.
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:
Header:
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:
- Take incoming header and payload
- Add
SECRET_KEY
that only the server and validator(s) know** - Put header, payload, and secret, through one-way encryption hash
- This creates a server-generated signature
- Check that the newly created signature matches the one sent in the JWT by the user
- 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 blacklist or a whitelist.
With a blacklist, whenever you want to invalidate a token, you would add it to the blacklist table, and whenever a user tries to use a JWT, you would always need to check that it is not on the blacklist.
With a whitelist, it is basically the same thing, but add every token to the whitelist as it is created, and remove it from the whitelist when invalidating. And every incoming JWT would only be accepted if it is on the whitelist.
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.
Additional 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 |
PS: If you enjoyed how this guide was written, you might like some of my other cheatsheets and quick reference guides. I've been working on collecting and showcasing them here.
Top comments (8)
I would highly recommend against maintaining a blacklist/whitelist, the need to do that signals a problem of architecture design. JWT represents authentication, you still need a mechanism for authorization. There isn't really a good reason to keep a database of jwts, it is more likely that is another attack service your service will be creating. If you find yourself needing to do that, you probably want take another look at why.
This isn't necessarily true. Using standard session continuation is what most providers of JWTs do to provide a secure way to stay logged in, even after a JWT expires. Instead a browser will "reauth" with the federated server, and get a new JWT the next time your website needs one. In general, you should be using an auth provider and not trying to roll your own JWTs.
Additionally it is worth noting:
This isn't strictly true either. You're using
HS256
token symmetric encryption in your example, which means that both the server and the validator need to be using the exact same validation method. Which actually means that unless you have a monolith, there are N + 1 identities which know that value. The actual recommendation is to useRS256
and the service signer will publish a public key any validator can use to verify the signature.These are all great points; I strongly agree that creating lists of JWTs and trying to force them into a session-shaped box is indicative that a developer might need to rethink their approach.
Re: Session continuation
You're right, and I've updated my post. It sounds like most people recommend using a short token duration, and auto-refreshing/exchanging as long as the site/app is kept open. Then the user only gets logged out after prolonged inactivity. It still rubs me the wrong way that this approach is recommended as a way to emulate "true" logout functionality, even by Auth0.
Re: Signing algorithms
Again, you're absolutely right. I missed that when researching JWTs, and have updated my post. I've kept my examples as symmetric, to reduce the complexity of the post, but added a disclaimer about symmetric vs asymmetric.
Great post. I have a QQ. In the the “Avoiding State” section, it seems like you don’t like the idea of changing the secret to invalidate JWTs, and I’m curious why that might be bad practice. Is only bad when used instead of a blacklist / whitelist approach?
Thanks! To answer your question, one of the primary reasons is that it leads to a bad user experience. If I change my server secret, it indiscriminately invalidates all circulating tokens, regardless of when they are created. If I'm a user, and I happened to have logged in just 20 seconds prior to a dev doing this, I'm going to be confused and maybe a little aggravated that I'm suddenly logged out again.
There are legitimate reasons to do this; if a secret key is leaked, you absolutely need to change the server-side key ASAP.
My problem with this method is when it is used as more of a crutch to avoid implementing a more robust session-based approach. At that point, the use of it, at least in my mind, is really a symptom of your system design not matching what is needed.
I try not to be opinionated about these things, but definitely adhere to the idea of "use what is best for your needs".
I see, that makes sense. It's better to change the key only if you actually want to revoke all user tokens. If you only want to revoke just some tokens, a different solution makes for a better user experience. Thanks.
Okta has a nice article from a few years ago about using JWTs for sessions - developer.okta.com/blog/2017/08/17...
Thanks for the link; they make a lot of really good points! You definitely lose a lot of the benefits of JWT over other auth approaches once you start trying to use them for session based stuff.
Thanks for the post.