How to Invalidate JWT Tokens Without Collecting Tokens
Tags: security, beginners
Published: 2022-12-05 01:43:24

Today I read [this article](https://dev.to/pragativerma18/how-jwts-could-be-dangerous-and-its-alternatives-3k3j) 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) | :white_check_mark: |
| Table grows without limit | :white_check_mark: |
| Extra check (DB or cache call) | :x: |

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:

```json
{
    "userId": 123456,
    "BlTimestamp": "2022-12-04T19:27:00Z"
}
}
```

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**.

```javascript
    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
        };
    }
```

**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!