Can I force a logout or expiration of a JWT token?

13,899

Solution 1

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.

Solution 2

Actually the best way to logout is just remove token from the client. And you can make lifetime of tokens short (5-15 minutes) and implement refresh tokens for additions security. In this case there are less chance for attacker to do something with your JWT

Share:
13,899
iBala
Author by

iBala

Updated on July 25, 2022

Comments

  • iBala
    iBala almost 2 years

    For authentication currently we are using JWT, so once a token is created it's created for a lifetime, and if we set a time expire, the token will expire.

    Is there any way to expire token?

    While clicking log out button, I need to destroy the token.

    I'm using ASP.NET Core WebAPI.

  • iBala
    iBala about 4 years
    Yes @Mateech, Tried this too but we are failing in Grey box testing.