The Dark Side of JWT: Why It's Not as Secure as You Think?
Tags: node, python, discuss, api
Published: 2023-12-29 19:18:59

No problem at all! I know, I used clickbait, but I genuinely want to hear your opinion on the following article because I truly believe that JWT is either overhyped or not as useful and stateless as everyone makes it out to be.

One of the problems of using JWT is the issue of signing out; the problem lies in the fact that, in principle, JWT is designed not to be stored in sessions and to have a valid expiration time. Therefore, the only way to sign out is to simply wait for that time period to expire.

But what if we want to have an API that doesn't manage sessions because we want it to be stateless for whatever reason, and we need to provide a secure way to log in to our API? Let's consider the following examples:

* **Issue**: A user wants to use our API on public computers, and we want to enable log out.
* **Solution**: The frontend deletes the stored JWT.
* **Current situation**: There is a valid token, but it is not stored on the user's computer.


![Request JWT](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/scfv9hd5pkwbicf3arki.png)

Now, let's imagine that along the way, an attacker has installed software to steal browser data, and now they have a copy of that token.

* **Issue**: A user has logged in on a public computer infected with malware, and an attacker has stolen their access token.
* **Solution (invalid)**: The frontend deleted the stored JWT, but the attacker still has it.
* **Situation**: The user's computer does not have a valid token, but the attacker does.
In the event that there is also a token refresh endpoint, this would mean that the attacker could be constantly refreshing the token.

![Steal JWT](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/53ftsrn5zz6mmgo4uw7y.png)

So we can already begin to see that JWTs, initially designed to be used without state, are not inherently reliable, leaving us with the following weaknesses:

1. We don't know how many valid tokens exist, as they could be stolen and refreshed multiple times.
2. We cannot log out.

Since a JWT has the ability to expire, these problems typically arise only when token refreshing is enabled, which is often done for a better user experience. However, this still leaves us with the issue that if a token is stolen, even if refresh is not possible, without proper server management, we cannot perform a sign-out. This means that during the time the token is valid, if it's in the possession of an attacker, they would have control over our account.

So, without states, we face the following problems:

- How to invalidate a token?
- How to know the available valid tokens?

Therefore, let's be honest: JWTs for applications where you want to stay logged in for a long time will require server-side management. This implies that, even though the API server itself is stateless, a database will be needed for such management. In my humble opinion, using JWT securely or at the user application level requires server-side states.

## Managing States
Once we acknowledge that JWT requires backend management to be secure, we could consider various storage options. In this case, we will use Redis for its speed. There's no need to store an entire token in the database (saving login sessions is a different story), and migration of data is unnecessary—users simply need to log in again. Additionally, for security reasons, in case a forced logout is necessary for all users, it's faster and safer, among other advantages.

When we start managing users in the database, two options arise: How many login sessions do we allow? In other words, do we create a new session with each login, or do we simply look for the active token and return it?

In the case of creating a token with each login, we could encounter a problem of credential creation saturation. This can be easily resolved with rate limiting, such as 10 per hour, and tokens with a duration of 1 hour. This way, there can never be more than 10 tokens created at once.

In the case of creating a token if it doesn't exist and returning the token if it does, the problem arises when you want to log in from another device and perform a token refresh. How do you handle sending it to the device that didn't initiate the token? Will you keep a socket open to send a notification? Will you include a wrapper or IP information in the token? This wouldn't be viable for two reasons: one, European data laws that could pose a problem, and two, IPs can change, or there could be multiple devices with the same IP using the web.

Let's say we're going to opt for the convenience of generating a token for each login with rate limiting. How do we handle signout?

What people typically suggest is to create a blacklist and then add the tokens you want to invalidate there. So, when you need to check a token, you first verify that it's not in the blacklist.

Now, this only solves the problem of invalidating a token, but we had another issue: how do I know which tokens are active because they've been stolen and I want to disable them, or in other words, "log out on all devices?" Managing all sessions, not just the invalidated ones, addresses this.

In fact, the approach I intend to present is using a whitelist instead of a blacklist. That is, every time you create a token, you save it, and each time you receive a request with a token, you check if it's in the whitelist.

To implement this with Redis, it can be done as follows.

- An ordered set will be created based on timestamp:
 - Key: User ID
 - Value: Token
 - Score: Timestamp
 - TTL: The token's lifespan, which will be the same for all tokens.
- A string will be created for each token:
 - Key: Token
 - Value: Token
 - TTL: The token's lifespan, which will be the same for all tokens.

The following operations will be implemented:

- add session
 - It will delete all sessions with a score lower than the current timestamp from the set.
 - It will create an element in the ordered set and another string.
- get sessions
 - It will delete all sessions with a score lower than the current timestamp from the set.
 - It will return the remaining sessions.
- is valid token
 - It will delete all sessions with a score lower than the current timestamp from the set.
 - It will check if the token exists; if it does, the token will be considered valid.
- Invalidate token
 - It will delete the token from both the ordered set and the string.
- Invalidate all sessions
 - It will delete the ordered set and all strings associated with tokens.

By doing this, we avoid the need to use search patterns and scans in Redis, although we duplicate information. We are assigning a TTL to the keys, so in the case of refresh, the key will remain alive. However, if it is not refreshed, just like the token expires, both the set and the string will expire, automatically invalidating them in Redis.

This way, we can control all sessions and perform signout because we only need to delete the keys from Redis.

On the flip side, we will be filling up Redis memory, but it's not something that takes up much space.

In conclusion, I believe that JWT is not as secure as it is often portrayed. It is simply popular, and people repeat the advantages without really studying what they are using. The truth is, JWT cannot be 100% secure (although nothing is) without proper backend management. Similarly, perhaps I lack information, but I think people often mindlessly repeat and add a blacklist without realizing other potential issues, such as how to determine the number of tokens.

This concludes the entire article, which is quite substantial. What are your thoughts? Do you believe JWT is as secure as it claims to be? Where do you use JWT, and what alternatives do you think are better and why?

Thank you.