DEV Community

José Pablo Ramírez Vargas
José Pablo Ramírez Vargas

Posted on • Edited on

How to Invalidate JWT Tokens Without Collecting Tokens

Today I read this article here at dev.to because it is, in my opinion, a topic everyone should care about.

The author explains well all the points, but did not volunteer what I think is the most practical way to invalidate tokens, so I'll do my best to complement that article here.

Unappealing Proposal

Let's start by looking at what people don't like: Invalidating tokens by blacklisting tokens in a database table. This takes space because tokens can get rather big, it forces you to do an extra check, and your table will just grow and grow.

Well, all but one of those problems can be eliminated.

Proposal

There's a better way to invalidate a JWT, and it is by its creation time.

All JWT's should have the iat claim, issued at. This is the time the token was issued/created. Instead of having a blacklist of tokens in the DB/Redis/Memcached, just have a much smaller list/table with user entries and the minimum date a token can be considered valid for that user. This table will only have a single entry per user. If a user goes through token invalidation multiple times, only the most recent one is important. So the table will asintotically grow to the maximum number of users.

But not only that, records in this table can also be deleted after we know for sure all previously issued tokens (that need blacklisting) have expired, making the table even smaller. This is a simple calculation: If now() - token TTL > stored timestamp, then the record can be safely eliminated.

So let's review my promise against the list of problems:

Problem Resolved?
Potentially big record size (due to size of token)
Table grows without limit
Extra check (DB or cache call)

It looks like I delivered.

Test Drive The Proposal

User X is an administrator, but you just received an email from your boss asking to demote user X to regular user. So you add the following record to our magic table:

{
    "userId": 123456,
    "BlTimestamp": "2022-12-04T19:27:00Z"
}
}
Enter fullscreen mode Exit fullscreen mode

Now your Security microservice (or subsystem or whatever), when it receives a request using User X's token issued 30 minutes ago and still valid, will undergo the iat check. "Well, well, well, look who's back asking for stuff, User X trying to be all macho deleting stuff. This token was issued at 2022-12-04T18:58:05Z, and I have a record that says I should not accept tokens from you if issued before 2022-12-04T19:27:00Z. No no no. Here's an HTTP 401 for you. Go re-authenticate."

Much simpler, correct? Let me know in the comments if you would like me to demo this in ASP.Net. Below is a core sample of code for NodeJS.

    validateToken: function (token) {
        let verifiedToken = null;
        try {
            verifiedToken = jwt.verify(token, config.jwt.secret);
        }
        catch (e) {
            console.error('Error verifying token: %o', e);
            return {
                valid: false
            };
        }
        // Standard validation succeeded.  Let's see about the iat:
        const globalInv = jwtInvalidationService.globalInvalidation();
        const userInv = jwtInvalidationService.userInvalidation(verifiedToken.name);
        let minimumIat = Math.max(globalInv, userInv);
        if (minimumIat) {
            minimumIat = new Date(minimumIat);
            console.debug('Token subject to minimum issued at verification: %s', minimumIat);
            const issuedAt = new Date(verifiedToken.iat * 1000);
            if (issuedAt < minimumIat) {
                console.warn("Token issued at %s for user %s is not acceptable.", issuedAt, verifiedToken.name);
                return {
                    valid: false
                };
            }
        }
        return {
            valid: true,
            token: verifiedToken
        };
    }
Enter fullscreen mode Exit fullscreen mode

NOTE: I know this extract may not make complete sense because there are things here that aren't obvious. For example, what's inside the token, or how the jwtInvalidationService works or the mechanism as to how to add invalidations is all missing from the code snippet. Make sure to follow me if you don't want to miss an upcoming article with the full project sample, which is about 200 lines of code.

Global Invalidation

The same table and mechanism works to invalidate every single token out there. Just add a blacklist timestamp with no user ID association. Then simply make sure any token you receive was issued after this time. If you ever globally invalidate, the table can actually be truncated, leaving only the global record since the global invalidation will supersede all existing per-user blacklist records, keeping the table small.

More Stuff

You can get more creative with this thing, if needed. You could evolve this model to account for token invalidation of only a particular security group by reviewing the list of roles in the claim (token) or database data. The sky is the limit, folks.

Happy coding!

Top comments (27)

Collapse
 
webjose profile image
José Pablo Ramírez Vargas

Hi. For most implementations where the size of the table doesn't usually exceed 100 records, I would run cleanup at the same time I do querying. Yes, I would be violating the idempotency of the "Get" nature of the operation in favor of a simplified architecture.

If you don't like that, or if the average number of records makes it so that the invested time in cleanup definitely takes a toll out of using the table values, then spin up a background service that does this cleanup every X minutes.

Also take the opportunity to clean up every time you globally invalidate, and every time a per-user invalidation takes place. Global invalidation cleanup is trivial (empty the table, then just leave the global record); per-user invalidation cleanup would be a DELETE statement with the WHERE condition set to what the paragraph describes as being the condition.

Collapse
 
asdfasdfaf20 profile image
y

Hey! I like this approach, but I just can't figure out how to manage multiple tokens. So, for my understanding, this is the situation I have in mind:

Image description(sorry I didn't have time for fancy graphics)

If the "BlTimestamp" is set to the date when User X logs out in Browser A, then everything's fine if that user continues in that browser.

But if User X opens up Browser B just a few minutes after logging in using Browser A, then I find this problem:

After the User X logs out in Browser A, the requests in Browser B would be denied because I would use the most recent invalidation date (when the user logs out), which should invalidate only token for Browser A.

Now, if the "most recent [token invalidation]" were to be the "exp" of token for Browser B, then any request after User X logs out Browser A, would no longer be unauthorized.

I'm not saying the article is wrong, I'm just confused here.

Collapse
 
webjose profile image
José Pablo Ramírez Vargas

Hello!

Logging out from a browser window should be limited to forgetting the token being used by that window and invalidation should not come into play.

This invalidation system is not meant to support log-out processes. To log out, remove the token from session storage. If you are passing the token in a cookie, then delete the cookie. That should be what the log-out process does.

Use this invalidation system to "log out from all devices" scenarios, or "user X's credentials were compromised, so invalidate all tokens for user X".

Let me know if this clarifies the question.

Question you might ask next: How can I have per-tab/browser window cookies? Not easily. I would generate a random GUID on load, then save it to session storage. This GUID would become part of the log-in data. Then the server sends a cookie whose name is/includes this GUID. Never tried this myself. It most certainly sounds like an interesting challenge.

Collapse
 
asdfasdfaf20 profile image
y

Oh, now I got you. It's clear. It makes complete sense in cases where I want to invalidate all tokens from a user.

I'll see how to deal with the question you mentioned. Thank you!

Collapse
 
ikehakinyemi profile image
17th_streetCode

Alright I might be the only one requesting for this but yeah, I want to see the laid out implementation. I will appreciate it.

Node.js, please.

Collapse
 
webjose profile image
José Pablo Ramírez Vargas

Hi! I have updated this entry showing the core logic of the iat check. If, like the note below the code snippet says, things don't make complete sense to you, wait for the upcoming article showing the entire project.

Collapse
 
ikehakinyemi profile image
17th_streetCode

Thank you for the update. Better understood now.

And yes, I'd love an upcoming article detailing the raw implementation of the service.

Ciao

Collapse
 
webjose profile image
José Pablo Ramírez Vargas
Collapse
 
nwa_eneh profile image
Tony

Brilliant technique. Thanks for the article

Collapse
 
webjose profile image
José Pablo Ramírez Vargas

No problem. I'm now exploring single-spa, if you are interested.

Collapse
 
nwa_eneh profile image
Tony

I'm interested. Any link to where you are doing that (github or articles)?

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

This series @ hashnode.com, where I hold my blog.

Collapse
 
iamazeez profile image
azeez

I am following this approach when user change his password so I wanted to user should be logout from all the other devices so its work very well in those scenario when we want to invalidate JWT token.

Collapse
 
iqbalatma profile image
Iqbal Atma Muliawan • Edited

Hi. This is a great article, it is very simple way to blacklist the token, but, what if I have multiple device or platform, let say mobile, desktop, and web, and i want to logout from web, it would be logout from all other device, is it correct ?

Collapse
 
webjose profile image
José Pablo Ramírez Vargas

As it is shown here, yes. But the beauty of it, is that you can further categorize the blacklisting process. Add an extra column to specify the token types that are to be invalidated. I would used a bitwise enumeration: 1 = web, 2 = desktop, 4 = mobile, etc. If I were to invalidate, I would include the type or types of token invalidated: 3 would mean web + desktop; 7 would mean all (web + desktop + mobile). You probably get the idea.

Collapse
 
iqbalatma profile image
Iqbal Atma Muliawan

yeah i got the idea. Thankyou, i love this approach, and this is more eficient approach so far (for jwt auth)

Collapse
 
happycoder profile image
SHUKLA123 • Edited

If user want to login with Incognito, and normal tab of browser. We will generate 2 JWTs for him. In my db, we only check the latest JWT which will provide user 401. But expected was 200 because user has loggedIn with two different places. Then, In this case it will fail.

Collapse
 
webjose profile image
José Pablo Ramírez Vargas • Edited

If I am understanding correctly, you are saying: If a user users 2 browsers (or one in regular mode and one in incognito mode), the user will have been issued 2 tokens. Yes, so far so good.

Then you say we invalidate the user's tokens based on creation time, which is what the article is about. For some reason you seem to disagree with both tokens being invalidated. This depends on the chronological order of events, but if you are thinking the following, then yes, both tokens get invalidated:

  1. User logs in from browser A.l User receives a valid token, TokenA.
  2. User logs in from browser B. User receives a valid token, TokenB.
  3. System administrator invalidates all tokens for this user using the current system time.

With this order of events, both tokens were invalidated. Why are you expecting one token to work?? I guess that's the part I don't understand.

UPDATE: Oh, ok. I re-read your answer. For some reason you are thinking incognito mode is special or something? Why do you think the browser used needs to have special treatment? Why do you think one token has to survive just because it came from X or Y browser in N or M mode? To me, that should not matter at all.

Collapse
 
happycoder profile image
SHUKLA123 • Edited

Hi
Lets take example
browser A : User receives a valid token, TokenA with userId 123 and timestamp - 2022-12-04T19:27:00Z.
DB call :
{
userId : 123,
timestamp : 2022-12-04T19:27:00Z
}

Same User gone for second browser to login again

browser B : User receives a valid token, TokenB with userId 123 and timestamp - 2022-12-04T20:27:00Z.
DB call :
{
userId : 123,
timestamp : 2022-12-04T20:27:00Z
}

As per your blog the browser A will logout with the api call which took the JWT have timestamp "2022-12-04T19:27:00Z" because the latest one is with the timestamp - "2022-12-04T20:27:00Z". But the user has not logout from any of the browser which will effect the user experience as he logout without any actual logout performed by the user.

How we can support multiple login of the same user with different browser or laptops etc.

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

Hi. For some reason I missed this comment. Apologies.

You seem to be thinking that issuing a token automatically invalidates previous tokens. This is only true if you program your system to work like this. At no point in the article do I say that the act of issuing a token necessarily invalidates all previous tokens for the user. I guess that's the misunderstanding we have here.

To me, if a user wants to have two windows open, then by all means, have them open. I'll gladly issue two independent tokens as long as the user can properly authenticate from both browser windows.

Collapse
 
gulshanaggarwal profile image
Gulshan Aggarwal

Yes, Iat check is a very good approach

Collapse
 
vagnerwentz profile image
Vagner Wentz

You could make an example using ASP.NET what do you think?

Collapse
 
webjose profile image
José Pablo Ramírez Vargas

Hello. I'll try to make some time for this, but I'm currently a bit swamped. Might take some time. If you're in a hurry, best to study the NodeJS + Express example and apply the same logic in C#.