A walk with JWT and security (II): JWT revocation strategies

The first one I’ll mention is not an actual revocation strategy, but some people argue that it is the best you can do with JWT to maintain its stateless nature while still mitigating its lack of built-in revocation. They say JWT tokens should have short expiration times, like, say, 15 minutes. This way, the time frame for an attacker to do some harm is reduced.

I don’t agree with that. I think it is still unacceptable not to have an actual server-side sign-out request. Shouldn’t the client do the right job destroying the token, a user could think that she has signed out from a server, but somebody coming just after them could be able to impersonate them.

Furthermore, it also has implications from a usability point of view. In some sites, such as online banking, it makes sense to force a user to sign in again if he has been inactive for 15 minutes, but in other scenarios, it can be a real hassle.

Signing Key Modification

Changing the signing key automatically invalidates all issued tokens, but it is not a way to invalidate single tokens. Doing that in a sign-out request would mean that everybody gets kicked out when just one requires leaving. Changing the signing key is something that must be done when there is some indication that it could have been compromised, but it doesn’t fit the scenario we are talking about.

Tokens Denylist

Keeping a denylist of tokens is the simplest actual revocation strategy for individual tokens that can be implemented. If we want to invalidate a token, we extract from it something that uniquely identifies it, like its “jti” claim, and we store this information in somewhere (for example, a database table). Then, each time a valid token arrives, the same information is extracted from it and queried against the denylist to decide whether to accept or refuse the token.

It is very important to notice that what is persisted is not the whole token but some information that uniquely identifies it. Should we persist the whole token, we will do that using encryption and adding a different salt for each one of them, treating it similarly to a password. However, for example, a “jti” claim alone is completely useless for an attacker.

A denylist strategy has some advantages:

But it also has some drawbacks:

User-Attached Token

This strategy is analogous to what is usually done with opaque tokens. Here, the information that uniquely identifies a token is stored attached to the same user record that it authorizes; for example, as another column in the user record. When a token with a valid signature comes in, that information is extracted and compared with the one attached to the user to decide whether authorization should be allowed. When we want to sign out the token, we simply change or nullify it.

In fact, these two steps in the checking process (fetching the user and comparing the token with the persisted information) can be reduced to a single one if we fetch the user through the token information. For example, we could have a “uid” column where we would store the “jti” claim of the current active token. When fetching the user, we would look that column up, and no matching would mean that the token is not valid. Of course, we need to be sure that the “uid” is actually unique.

As in the case of the denylist strategy, it is very important to understand that it is not the whole token that is persisted but a unique representation of it to avoid security concerns related to secrets storage (see the denylist section for details).

User-attached tokens have several advantages that, for me, make it a better idea than the denylist strategy:

On the other side, it also has one inconvenience:

Waiting for dev.

I happen to be one of the roughly 106,000,000,000 human beings who have inhabited the Earth at some point. In this thing called society, I have taken on the role of a software developer. If I can assist you with anything or if you'd like to chat, please feel free to reach out to me.