As mentioned before, JWT has some drawbacks that make it unsuitable as an authentication system. However, there are ways to work around these drawbacks and make JWT more secure.
One way to protect our system is to blacklist JWT tokens (although JWT is stateless and was not designed to be blacklisted). But as they say, tools can be used in ways they were not designed for.
Here's how it works:
First, make sure to add the issued_at
(iat) claim to the payload of the JWT token when the server creates it. It's a timestamp that represents the time when the token was issued/created. For example:
{
"name": "John Doe",
"user_id": "123",
"iat": 1643961600
}
Then, this is how the process will work when a user logs out or you want to block someone from accessing the system through an admin dashboard or something:
- When a user logs out, we will add their
user_id
andminimum_issued_at
to a blacklist table in the database.
INSERT INTO blacklist (user_id, minimum_issued_at) VALUES (1, NOW());
- When a user tries to access the system via a JWT token, it will go through this process:
- The server will check if the JWT token is valid (not expired, not tampered with, etc.).
- If the JWT token is valid, the server will check if the
user_id
that is in the JWT payload exists in the blacklist table. - If it exists, the server will check if the
iat
of the token is greater thanminimum_issued_at
in the blacklist table for thatuser_id
.- NOTE: If the user has multiple records in the blacklist table, the server will check for the record with the latest
minimum_issued_at
value since it's the most recent one.
- NOTE: If the user has multiple records in the blacklist table, the server will check for the record with the latest
- If the
iat
of the token is greater thanminimum_issued_at
, the server will allow the user to access the system. - If it's not greater, the server will deny the user access to the system (goodbye, you are blocked ๐ here is a 403 status code).
Now, the blacklist table will have a lot of records over time (which is not good for performance/efficiency). So, we need to clean it up from time to time. We can do this by creating a cron job that runs every day and deletes all records that have minimum_issued_at
less than the current time minus JWT token TTL (Time To Live). This way, we can keep the blacklist table clean and small.
DELETE FROM blacklist WHERE minimum_issued_at < NOW() - INTERVAL 15 MINUTE;
Additionally, you can also do global token invalidation by changing the secret key (not recommended, but it's an option). Or in the case of the Blacklist table, you can insert user_id
with the value "all" and minimum_issued_at
with the current time. And let the server check for this record first before checking for the user's record. If it exists, deny access to the system.
Moreover, you can add more columns to the blacklist table to make it more flexible. For example, you can add a platform
column to store the platform that the token was issued for (web, mobile, etc.). You can add a device
column to store the device that the token was issued for (laptop, phone, etc.). This way, you can invalidate tokens based on the platform or device too.
Drawbacks of Blacklisting JWT:
- You need to query the database for each request to check if the token is blacklisted or not (since the table will be cleaned up from time to time, it will be small and fast to query + you can add an index on
user_id
andminimum_issued_at
columns to make it faster to query + you can use a cache like Redis to store the blacklist table in memory to make it even faster).
Conclusion:
- Blacklisting JWT tokens is a good way to make JWT more secure and flexible even though it's not designed to be blacklisted. It's a good way to work around the drawbacks of JWT as an authentication system.
Resources:
Top comments (11)
One thing that you could do instead of using a cron is to use database triggers; something like:
CREATE TRIGGER delete_blacklist_rows_trigger AFTER SELECT ON blacklist EXECUTE FUNCTION delete_expired_rows();
If I logout on my pc, I don't necessarily want to be logged out as well on my phone or tablet. Yet, this is exactly what your "solution" does. It kills all sessions on all devices at once, since all tokens having been issued before the logout are invalidated... And it does not even touch the issue of refresh tokens.
No, you need the JWT payload to have device info or any other info you want.. and check against that. For example, Kill the token that has device โmobileโ for user โ123โ
Well, you certainly could add a
plaform
ordevice
to your JWT blacklist to do some "browser fingerprinting" but of course this has its limits. I'm not even sure you could reliably distinguish between a user's phone and tablet using the same OS/browser, or between a PC and a Laptop using same OS/Browser. Blacklisting one withminimum_issued_at
is likely to kill the other session too ...or you'll need to put a lot of extra work. Last but not least this nullifies the main benefit of using JWTs, namely to avoid DB lookups.its not โalotโ of extra work.. whenever you create the JWT token for the user just add the fields in JWT payload you want to check against such as device, user_agant, location.. and whatever else you want, the sky is the limit.
JWT definitely was not designed to have DB lookup as I mentioned in the post, but the DB table here will be very small since it will only have invalid tokens when user logs out/blocked and will be cleaned periodically. This is a work around for my drawbacks that were mentioned in my previous post.
JWT is just a tool like many tools out there, take it into your advantage as you need.
What is the use case of blacklisting tokens?
Why not just let them expire naturally?
If you have high security requirements (like a bank) -> you have/use 2fa anyway.
Generally if you log out with a JWT, most of the time what will happen is you will remove the token from local storage/cookies. Let's say you have an authentication strategy where you're not using refresh tokens (which I recommend you should do), but long lived JWTs such as 1 DAY, 5 DAY, 30 DAY etc. So, if a user "logs out", the user may believe they are logged out but the JWT is technically still usable. By adding them to a blacklist, you have a mechanism to block any further usage of the JWT. Further, you can use the tokens minimum_issued_at to expire the row in your postgres/redis/store after the JWT will have expired and become unusable.
Another good example of being able to blacklist tokens is it provides a mechanism to provide user functionality such as "Force logout on devices" that you might see on things like Gmail and so forth.
Isn't this very rare (purely theoretical) use case?
If you delete the JWT from users device memory and storage ->
How does someone use a "technically still usable" token if its nowhere to be found?
I assume
It boils down to a principle I adopted a long time ago: Never trust user input.
You also confuse HTTP codes. 403 is when you are authenticated but access to the resource is forbidden because of insufficient rights. When you are logged out, you become 401 Unauthorized.
Since its a blacklist I choose 403 :)