Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 481
iBala Avatar asked Apr 22 '20 15:04

iBala


3 Answers

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.

like image 131
iBala Avatar answered Oct 16 '22 07:10

iBala


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

like image 40
Mateech Avatar answered Oct 16 '22 07:10

Mateech


If you have implemented the login scenario with the refresh token, You can remove the refresh token from the server and then , and then you should remove the token from the client.

like image 1
maryam ghoreishi Avatar answered Oct 16 '22 07:10

maryam ghoreishi