I think cancelling JWT is the best way to handle logout. Piotr explained well in his blog: Cancel JWT tokens
We will start with the interface:
public interface ITokenManager
{
Task<bool> IsCurrentActiveToken();
Task DeactivateCurrentAsync();
Task<bool> IsActiveAsync(string token);
Task DeactivateAsync(string token);
}
And process with its implementation, where the basic idea is to keep
track of deactivated tokens only and remove them from a cache when not
needed anymore (meaning when the expiry time passed) – they will be no
longer valid anyway.
public class TokenManager : ITokenManager
{
private readonly IDistributedCache _cache;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IOptions<JwtOptions> _jwtOptions;
public TokenManager(IDistributedCache cache,
IHttpContextAccessor httpContextAccessor,
IOptions<JwtOptions> jwtOptions
)
{
_cache = cache;
_httpContextAccessor = httpContextAccessor;
_jwtOptions = jwtOptions;
}
public async Task<bool> IsCurrentActiveToken()
=> await IsActiveAsync(GetCurrentAsync());
public async Task DeactivateCurrentAsync()
=> await DeactivateAsync(GetCurrentAsync());
public async Task<bool> IsActiveAsync(string token)
=> await _cache.GetStringAsync(GetKey(token)) == null;
public async Task DeactivateAsync(string token)
=> await _cache.SetStringAsync(GetKey(token),
" ", new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow =
TimeSpan.FromMinutes(_jwtOptions.Value.ExpiryMinutes)
});
private string GetCurrentAsync()
{
var authorizationHeader = _httpContextAccessor
.HttpContext.Request.Headers["authorization"];
return authorizationHeader == StringValues.Empty
? string.Empty
: authorizationHeader.Single().Split(" ").Last();
}
private static string GetKey(string token)
=> $"tokens:{token}:deactivated";
}
As you can see, there are 2 helper methods that will use the current
HttpContext in order to make things even easier.
Next, let’s create a middleware that will check if the token was
deactivated or not. That’s the reason why we should keep them in cache
– hitting the database with every request instead would probably kill
your app sooner or later (or at least make it really, really slow):
public class TokenManagerMiddleware : IMiddleware
{
private readonly ITokenManager _tokenManager;
public TokenManagerMiddleware(ITokenManager tokenManager)
{
_tokenManager = tokenManager;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (await _tokenManager.IsCurrentActiveToken())
{
await next(context);
return;
}
context.Response.StatusCode = (int) HttpStatusCode.Unauthorized;
}
}
Eventually, let’s finish our journey with implementing an endpoint for
canceling the tokens:
[HttpPost("tokens/cancel")]
public async Task<IActionResult> CancelAccessToken()
{
await _tokenManager.DeactivateCurrentAsync();
return NoContent();
}
For sure, we could make it more sophisticated, via passing the token
via URL, or by canceling all of the existing user tokens at once
(which would require an additional implementation to keep track of
them), yet this is a basic sample that just works.
Make sure that you will register the required dependencies in your
container and configure the middleware:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddTransient<TokenManagerMiddleware>();
services.AddTransient<ITokenManager, Services.TokenManager>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddDistributedRedisCache(r => { r.Configuration = Configuration["redis:connectionString"];
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
...
app.UseAuthentication();
app.UseMiddleware<TokenManagerMiddleware>();
app.UseMvc();
}
And provide a configuration for Redis in appsettings.json file:
"redis": {
"connectionString": "localhost"
}
Try to run the application now and invoke the token cancellation[sic]
endpoint – that’s it.