DEV Community 👩‍💻👨‍💻

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

Posted on • Updated 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 (13)

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
 
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
 
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
 
happycoder profile image
SHUKLA123 • Edited on

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 on

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 on

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.

Collapse
 
gulshanaggarwal profile image
Gulshan Aggarwal

Yes, Iat check is a very good approach

Collapse
 
joshuaamaju profile image
Joshua Amaju

Interesting idea 👍🏽, will try it out and provide feedback

Join us at DEV Want to join the conversation?
 

It's easy! Become a DEV member to follow this post, comment, and more.