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"
}
}
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
};
}
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)
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.
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:
(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.
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.
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!
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.
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.Thank you for the update. Better understood now.
And yes, I'd love an upcoming article detailing the raw implementation of the service.
Ciao
Ok, the full tutorial in NodeJS + Express is out.
Invalidating JSON Web Tokens (JWT): NodeJS + Express
José Pablo Ramírez Vargas ・ Dec 11 ・ 13 min read
Brilliant technique. Thanks for the article
No problem. I'm now exploring
single-spa
, if you are interested.I'm interested. Any link to where you are doing that (github or articles)?
This series @ hashnode.com, where I hold my blog.
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.
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 ?
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.
yeah i got the idea. Thankyou, i love this approach, and this is more eficient approach so far (for jwt auth)
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.
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:
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.
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.
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.
Yes, Iat check is a very good approach
You could make an example using ASP.NET what do you think?
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#.