DEV Community

Cover image for The Dark Side of JWT: Why It's Not as Secure as You Think?
Tlaloc-Es
Tlaloc-Es

Posted on • Updated on

The Dark Side of JWT: Why It's Not as Secure as You Think?

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

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

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.

Top comments (15)

Collapse
 
miketalbot profile image
Mike Talbot ⭐

Storing the JWT in an httpOnly Secure cookie reduces the chance of it being stolen significantly, but you've still got to keep a Redis log of disabled keys for sure. The case may be that the user's account is invalidated by an administrator etc.

Collapse
 
tlaloces profile image
Tlaloc-Es

Thank you for your response, but regarding having disabled keys in Redis, do you think it's necessary? I mean, if you have a whitelist, even if a valid token enters, you would still need to check in Redis that the key exists. If it doesn't exist, either because it expired or was deleted to disable it, then even if the user provides a valid JWT, they would be denied access since it's not on the whitelist. Could you give me an example of why to use the blacklist to see if I understand?

Collapse
 
miketalbot profile image
Mike Talbot ⭐

Very few tokens will be blacklisted, therefore the amount of storage used to maintain this list is minimal. Blacklist keys need to only last as long as tokens remain valid, so most of the time, there are no keys being maintained at all.

Whitelisting tokens is creating a secondary auth database with cost and performance implications.

Thread Thread
 
tlaloces profile image
Tlaloc-Es

I understand the point, but I think that once you have to put a token on the blacklist, you no longer know, depending on how it has been implemented, whether the attacker hasn't generated another refresh token and someone has your session. I believe that managing JWT without having control over the tokens and following typical web tutorials where everything is not made clear can be insecure. I'm starting to think that using JWT is more of a hype than something useful. By the way, I don't know if you've seen this video; you might find it interesting: youtube.com/watch?v=pYeekwv3vC4

Collapse
 
webjose profile image
José Pablo Ramírez Vargas
Collapse
 
tlaloces profile image
Tlaloc-Es

Gracias Jose for your answer, Your approach is really interesting, and it brings up points I hadn't considered, such as not directly saving the token. I wanted to know what you think, for example, about using invalidation for log-out? Is it something you would do, or would it simply not be a feature to consider? In this case, for instance, every time you log in, would you always need to have a record in Redis or wherever with the "iat" (issued at) timestamp, or would you approach it differently?

Thread Thread
 
webjose profile image
José Pablo Ramírez Vargas

I would only use IAT invalidation for "log out from everywhere". Not for regular log outs.

Thread Thread
 
tlaloces profile image
Tlaloc-Es

And how do you refresh the token to differentiate it from the access token? Do you set any attribute with a specific value?

Thread Thread
 
webjose profile image
José Pablo Ramírez Vargas

I suppose you mean to ask "how do you create the refresh token to differentiate it from the access token?". If yes, then a payload attribute, most commonly tokenType or a shortened version like ttyp. Your refresh tokens (or all your tokens) can be given a unique ID and the refresh token ID could become part of the access token payload if you need fancy tracking.

Collapse
 
dagnelies profile image
Arnaud Dagnelies • Edited

For user authentication/sessions, I'm all on your side. Good ol' session cookies are both simpler and safer than JWTs. Cookie is there, user is logged in, cookie is gone, user is logged out. Dead simple, secure, efficient. Nevertheless, IMHO, the main purpose of JWT is another: usage of a third-party API. In that context, when a service "grants permission" to another, JWTs make much more sense since it's decoupled from user authentication/sessions altogether. The API may even be used in a background services while the user is offline.

Collapse
 
tlaloces profile image
Tlaloc-Es

Thank you for your point of view; I think it's interesting. I really thought I was wrong with all the hype about JWT, but as you say, I believe sessions are the simple option that has what is needed.

Collapse
 
syeo66 profile image
Red Ochsenbein (he/him)

I guess this is another case of 'it depends'. How long is the window a potential attacker has when he's got a token. He should not be able to do a refresh with the token alone. Was he able to retrieve the refresh token, too? What are the risks. Maybe you'd have to implement an additional security check for certain actions? There are many ways to make JWT work in a secure enough way. The question is always 'what harm could be done?' and mitigate.

Collapse
 
tlaloces profile image
Tlaloc-Es

Hello, thanks for your response. As you mentioned, it's another case of "it depends," and of course, depending on the implementation of the refresh token, it could be even worse. Still, the more you delve into how you have to implement JWT instead of sessions, for example, it starts to make less sense for them to sell it to you as easy, secure, fast, and without sessions. In the end, to ensure a certain level of security, it depends on the opposite. I just watched this video youtube.com/watch?v=pYeekwv3vC4 that presents other points in favor of or against JWT, which I find interesting. What do you think? Do you usually use JWT or sessions?

Collapse
 
ozair0 profile image
Ozair

It was a great post and I agree with you but what do you think about Redis in a microservices architecture? is it a good idea to use 1 Redis instance among all the running services?

Collapse
 
tlaloces profile image
Tlaloc-Es

Hello, thank you, I'm glad you liked the post. Regarding Redis, I don't think it's a drawback to use it for data that can be volatile, such as caching or a shopping list – I know, I know, those are typical examples. However, I haven't had the need to implement more complex things. Personally, I've used it for caching and messaging on a few occasions, although I've mostly used Kafka or Pulsar.