JWT Token Revocation in ASP.NET
September 14th, 2022JWT tokens are a popular way to implement authentication and authorization. The problem with JWT tokens is that they don’t intrinsically provide a revocation mechanism: tokens are valid until they expire.
Let’s take a look at one way to add that feature to ASP.NET projects.
Preamble
I’m going to assume that you have authentication and authorization working with JWT tokens in your ASP.NET Web API already: users of your app can sign in — signing out is the problem.
Perhaps your project has a mobile or single page application front-end that sends user credentials to a RESTful-ish1 API that responds with a JWT token if they are valid. The presence of the token indicates authentication (the user has proven their identity) and the claims in the token, if any, define the roles the user is authorized to perform.
The front-end will include the encoded JWT token with each request as a Bearer
token in the Authorization
header that the server will extract, decode, and validate before processing each request. If anything about the token doesn’t pass muster the server immediately rejects the request.
The Problem
As the token is stored on the client — precisely how is out of scope for this post — without any mechanism for the server-side code to control its lifetime; how can the server decide when to deny a token even when it hasn’t expired?
There is a solution to this in the OWASP Cheat Sheet Series and that is to implement a sort of reverse session key management system: a block list that records which tokens that haven’t yet expired should, nevertheless, be rejected. This is subtly different to normal session key stores. Those record every active session key and the server pays for that with increased memory usage: thousands of users means thousands of session keys in memory.
The OWASP solution is rather elegant in that only those tokens that are currently active but should be disavowed need to be recorded and, as JWT tokens expire by design, that record only needs to be maintained until the token has lapsed. Rather than recording thousands of tokens for thousands of users, indefinitely, the server now only needs to record the subset of tokens where those tokens have been revoked (perhaps a low percentage of the total) and only for the remaining time while the token would otherwise be valid2.
The Solution
For all the wordage so far, the solution is pleasantly simple: a custom token lifetime validator.
The code
When configuring the ASP.NET dependency injection container, the following code is used to add JWT Bearer Token support:
And the members of TokenValidationParameters
is where we will find what we are looking for: the LifetimeValidator
property.
When a lifetime validator delegate is provided it will be called3 for every token that the API receives and has the opportunity to decide whether the token is still considered valid. A default implementation might just check the ValidFrom
and ValidTo
properties of a JWT token, but this is an ideal place to add a check against a block list too.
First, lets plug in that custom lifetime validation, then discuss what it does:
This code runs during the container configuration phase. So, frustratingly, the container can’t be called upon to provide an instance of JwtTokenLifetimeManager
, a custom class that contains the token-management logic. Instead, an instance is created manually and then added to the container as a singleton as it will be required in a controller where the sign out functionality is implemented. It looks like this:
For this example, it maintains an internal ConcurrentDictionary<string, DateTime>
that records token signatures and expiry timestamps. The ValidateTokenLifetime
logic then does the usual time-based checks and, if the token is currently valid, then checks to see if it is in the list of disavowed tokens.
In a more production-ready version of this code I would recommend making the DisavowedSignatures
storage pluggable, perhaps backed by something like Redis, which would help this scale more easily.
The second method that the JwtTokenLifetimeManager
provides is SignOut
. This simply records the token’s signature and expiry timestamp then trims any that are in that list but have lapsed: a bit of housekeeping to keep the list lean.
Then, finally, the sign out controller action just retrieves the Authorization
header, trims the Bearer
prefix and passes the decoded JWT token to the TokenLifetimeManager
.
Conclusion
I think this approach, suggested in the OWASP Cheat Sheets, is rather elegant and, as it turns out, pretty straight-forward to implement in ASP.NET.
This post shows a much-simplified example of how it could be implemented; if you do follow these ideas do make sure to implement some error handling and consider making the disavowed token storage a separate, injectable type that stores its data in an out-of-proc store otherwise there will be all sorts of odd signed-in then not signed-in problems when scaling beyond a single host.
Which leads me to a closing question: is there a more elegant way to handle dependencies that need to exist before the builder.build()
call, like the JwtTokenLifetimeManager
in this example?
-
Everyone starts with a RESTful API… ↩
-
A problem almost perfectly specced for Redis EXPIRE. ↩
-
Regardless of the value of the
ValidateLifetime
property, it should be noted. ↩