Sometimes, people take technologies that are intended to solve a narrow problem and start applying them broadly. The problem may appear similar, but utilizing unique technologies to solve general issues could create unanticipated consequences. To use a metaphor, if you are a hammer, everything looks like a nail. JWT is one such technology.
There are many in-depth articles and videos from SMEs of companies like Okta talking about the potential dangers and inefficiencies of using JWT tokens. Yet, these warnings are overshadowed by marketers, YouTubers, bloggers, course creators, and others who knowingly or unknowingly promote it.
If you look at many of these videos and articles, they all just talk about the perceived benefits of JWT and ignore the deficiency. More specifically, they just show how to use it but don’t talk about revocations and additional complexities that JWT adds in a real production environment. They also never compare it with the existing battle-tested approaches deep enough to really weigh the pros and cons.
Or maybe it’s the perfect, buzzworthy, and friendly name that’s leading to its popularity. “JSON” (generally well-liked), “Web”(for web), and “Token”(implies stateless) make people think that it’s perfect for their web authentication job.
So I think this is a case where the marketing has beaten engineers and security experts. But, it’s not all bad because there are regular long and passionate debates about JWT on Hacker News (see here, here and, here), so there is hope.
If you think about it, these constant debates themselves should be a red flag because you should never see such debates, especially in the security realm. Security should be binary. Either technology is secure or it’s not.
In any case, in this blog post, I’d like to focus on the potential dangers of using JWT and also talk about a battle-tested solution that’s been around for a decade.
For further understanding, when I talk about JWT, I mean “stateless JWT,” which is the primary reason for the popularity of JWTs and the biggest reason to use a JWT in the first place. Also, I’ve listed all the other articles in the resources section down below that go into the nitty-gritty of JWT.
Before we understand why it’s dangerous, let’s first understand how it works by taking an example use case.
Imagine you are using Twitter. You log in, write a tweet, like a tweet, then retweet someone else’s tweet. So you did four actions. For each action, you need to be authenticated and authorized before you can perform that specific action.
Below is what happens in the traditional approach.
The problem is that step four is slow and needs to be repeated for every single action the user does. So every single API call leads to at least two slow DB calls which can slow down the overall response time.
There are different ways of achieving this.
Now let’s look at the JWT way.
JWT, especially when used as a session, attempts to solve the problem by completely eliminating the database lookup.
The main idea is to store the user’s info in the session token itself! So instead of some long random string, store the actual user info in the session token itself. And to secure it, have part of the token signed using a secret that’s only known to the server.
So even though the client and the server can see the user info part of the token, the second part, the signed part, can only be verified by the server.
In the picture below, the pink section of the token has the payload (user’s info) and can be seen by both the client or the server.
But the blue part is signed using a secret string, the header, and the payload itself. And so if the client tampers with the payload (say impersonates a different user), the signature will be different and won’t be authenticated.
Here is how our use case would look like with JWT:
Going forward for every user action, the server simply verifies the signed part, gets the user info, and lets the user do that action. Thus completely skipping the DB call.
But there is one additional and important thing to know about the JWT tokens. And that is that it uses an expiration time to expire itself. It’s typically set to 5 minutes to 30 minutes. And because it’s self-contained, you can’t easily revoke/invalidate/update it. This is really where the crux of the problem lies.
The biggest problem with JWT is the token revoke problem. Since it continues to work until it expires, the server has no easy way to revoke it.
Below are some use cases that’d make this dangerous.
Imagine you logged out from Twitter after tweeting. You’d think that you are logged out of the server, but that’s not the case. Because JWT is self-contained and will continue to work until it expires. This could be 5 minutes or 30 minutes or whatever the duration that’s set as part of the token. So if someone gets access to that token during that time, they can continue to access it until it expires.
Imagine you are a moderator of Twitter or some online real-time game where real users are using the system. And as a moderator, you want to quickly block someone from abusing the system. You can’t, again for the same reason. Even after you block, the user will continue to have access to the server until the token expires.
Imagine the user is an admin and got demoted to a regular user with fewer permissions. Again this won’t take effect immediately and the user will continue to be an admin until the token expires.
It’s been found that many libraries that implement JWT have had many security issues over the years and even the spec itself had security issues. Even Auth0 itself, who promotes JWT got hit with an issue.
In many complex real-world apps, you may need to store a ton of different information. And storing it in the JWT tokens could exceed the allowed URL length or cookie lengths causing problems. Also, you are now potentially sending a large volume of data on every request.
In many real-world apps, servers have to maintain the user’s IP and track APIs for rate-limiting and IP-whitelisting. So you’ll need to use a blazing fast database anyway. To think somehow your app becomes stateless with JWT is just not realistic.
One popular solution is to store a list of “revoked tokens” in a database and check it for every call. And if the token is part of that revoked list, then block the user from taking the next action. But then now you are making that extra call to the DB to check if the token is revoked and so deceives the purpose of JWT altogether.
Although JWT does eliminate the database lookup, it introduces security issues and other complexities while doing so. Security is binary—either it’s secure or it’s not. Thus making it dangerous to use JWT for user sessions.
There are scenarios where you are doing server-to-server (or microservice-to-microservice) communication in the backend and one service could generate a JWT token to send it to a different service for authorization purposes. And other narrow places, such as reset password, where you can send a JWT token as a one-time short-lived token to verify the user’s email.
The solution is to not use JWT at all for session purposes. But instead, do the traditional, but battle-tested way more efficiently.
I.e. make the database lookup so blazing fast (sub-millisecond) that the additional call won’t matter.
Is there any database out there so blazing fast that it can serve millions of requests in sub-milliseconds?
Of course, there is. It’s called Redis! Thousands of companies that serve billions of users daily use Redis just for this exact purpose!
And Redis Enterprise is an enhanced version of the Redis OSS that provides 99.999% availability and can serve trillions of requests. It can be used as free software on private clouds or in the cloud on any of the top-3 clouds.
What’s more? Redis Enterprise has now evolved from just being a cache or session store, to a fully-fledged multi-model database with its module ecosystem that runs natively with core Redis. For example, you can use JSON (10x faster vs. the market leader) and essentially have a real-time MongoDB-like database, or use the Search and Query feature (4–100x faster) and implement real-time full-text search like Algolia.
If you simply use Redis as a session store and some other Database as a primary database, this is how your architecture would look. One thing to note is that Redis Enterprise provides four types of caching: Cache-aside (Lazy-loading), Write-Behind (Write-Back), Write-Through, and Read-replica, unlike Redis OSS which only provides one (Cache-aside).
Note that the lightning emoji indicates a blazing fast speed. And the snail emoji indicates slow speed.
As mentioned earlier, you can also use Redis as a primary database for your entire data layer. In this scenario, your architecture becomes much simpler and basically, everything becomes blazing fast.
Of course, companies use Redis not just as a standalone database but as a cluster of geographically distributed databases.