In a past article, I wrote about JWTs, how to generate one and how to use them for authorization. JSON Web Tokens, however, have one major drawback. Once it is generated and submitted to the client, it can’t be easily made invalid. This is a big problem if the JWT got leaked and it did not expire (or worse, it does NOT have an expiration date). That is why it is important to make sure that your JWT can be invalidated at the server-side and I will show you two methods to do this.
Method 1: Blacklisting
The easiest way, at least at first glance, is to have a way of blacklisting a JWT once it is compromised. You can store all compromised JWTs in the database and when an authorization request is received, you check that the JWT used is not part of this list. This approach has one big disadvantage though: the DB can become a bottleneck due to the amount of data that you need to retrieve.
For small systems, this is not a major problem, but when you have thousands of requests per second, retrieving a full list of JWTs or going to the database to do a search for the one received can really slow down processing. This can be partially mitigated by using in-memory lists and caching the blacklisted JWTs, but this can also pose a problem because you don’t know if the cache is up-to-date. For example, if your cache refreshes at 15 minutes intervals, there still is that 15 minute window when a compromised JWT can still be used.
To mitigate this you can make a method for invalidating the cache once a new blacklisted JWT is added. Also, what do you do for distributed systems that have multiple instances of the same service? Another problem is that the database has to be cleaned to remove JWTs that have expired and will fail validation even if not black-listed. All these will definitely add to the complexity of the system, will make it more error-prone, and can slow down overall processing, if not done correctly. So, here is a better way!
Method 2: Versioning the JWT
The JWT can hold as much data as we need to validate it. The claims we insert during the build can be used for many purposes. So, let’s use one of the claims to also validate that the JWT is not blacklisted. I am not talking about an expiration date, this is something completely different, but of some information that we can easily correlate with the server and see if it was not marked as being invalid.
I am thinking of using a “version” for the JWT. When we build the JWT for the first time for an entity (a user, a functionality, or some other category), we also store the version of the JWT for that entity. This can be in the database and it won’t provide a big overhead during processing. I will explain why a bit later.
public class MyServer {
public String generateJwt(String userId, List<String> , String entityId, String version) {
Algorithm algorithm = Algorithm.HMAC256("mySecret");
JWTCreator.Builder jwtBuilder = JWT.create()
.withIssuer("myServer")
.withClaim("accessGrantedBy", userId)
.withClaim("accessArea", accessAreas)
.withClaim("entityId", entityId)
.withClaim("version", version);
return jwtBuilder.sign(algorithm);
}
}
So, now we have the following additional claims: entityId, version. Keep in mind that the entityId can be the userId if you are generating the access for a specific user. I only used entity to be more generic. When a JWT is received, as part of the validation process, you also check that the version is the same as the one stored on the server. Only then you consider it valid.
public boolean hasAccess(DecodedJWT jwt, String areaToAccess, String entityId) {
if (jwt == null) return false;
int version = entityDao.getJwtVersion(entityId);
int versionInJwt = jwt.getClaim("version");
String entityIdInJwt = jwt.getClaim("entityId");
if (!entityId.equals(entityIdInJwt) || version != versionInJwt) return false;
return jwt.getClaim("accessArea")
.asList(String.class)
.contains(areaToAccess);
}
If a JWT is compromised, all you have to do is increment the version on the server by one and the JWT validation will fail.
You will still need access to the database, but with good design, you won’t need any additional queries than what you will be doing anyway. For example, if the JWT is for a user, will most probably need the user information either way later on. If the JWT version is in the same table, you can retrieve it in one go along with the data you already retrieve. Even if there is no way for the JWT version to be in the same table, the access pattern will be really fast since you will do direct search by a primary key (the entityId) and the data received back is really small: a number.
Also, don’t stress too much about big number. You don’t need to increment the version every time you generate a new JWT for the entity. If there was no data compromised when the previous JWT has expired, you can reuse the same version. Only if and when a token gets compromised you need to change the version for that entity.
Advantages of versioning the JWT
- No need to keep a big blacklist of compromised JWTs
- Easy and direct access to the current version on the server, most probably without any additional DB access
- No need to synchronize data between server instances since this is done automatically by the DB
- No cleanup needed for expired JWTs
- Easy to mass invalidate JWTs in case of a wider system breach
Article originally posted on my personal site under How to invalidate a JWT in Java
Top comments (2)
The downside I see in this approach is that you have to keep track of the incrementing number (or use a timestamp-based value), which means updating the database for every JWT created.
Another approach for revoking a JWT is to store the timestamp when the user ID was last revoked, then look at the "iat" field in the JWT. If a revocation date is found for the user, only tokens issued after that date are valid.
Having a separate global timestamp is also good for mass revocation (say in the case where your signing key is compromised).
The approach with storing the timestamp of the revocation work really well as long as you only have one JWT for a user/application. However, if for the specified account there are multiple JWTs (for example one used by the user and one by some automated tool), you will revoke both. There are workaround though, I believe, so I still consider your idea to be good :)
Thanks for the suggestion.