How to invalidate tokens after password change

10,604

Solution 1

The easiest way to revoke/invalidate is probably just to remove the token on the client and pray nobody will hijack it and abuse it.

Your approach with "accessCode" column would work but I would be worried about the performance.

The other and probably the better way would be to black-list tokens in some database. I think Redis would be the best for this as it supports timeouts via EXPIRE so you can just set it to the same value as you have in your JWT token. And when the token expires it will automatically remove.

You will need fast response time for this as you will have to check if the token is still valid (not in the black-list or different accessCode) on each request that requires authorization and that means calling your database with invalidated tokens on each request.


Refresh tokens are not the solution

Some people recommend using long-lived refresh tokens and short-lived access tokens. You can set access token to let's say expire in 10 minutes and when the password change, the token will still be valid for 10 minutes but then it will expire and you will have to use the refresh token to acquire the new access token. Personally, I'm a bit skeptical about this because refresh token can be hijacked as well: http://appetere.com/post/how-to-renew-access-tokens and then you will need a way to invalidate them as well so, in the end, you can't avoid storing them somewhere.


ASP.NET Core implementation using StackExchange.Redis

You're using ASP.NET Core so you will need to find a way how to add custom JWT validation logic to check if the token was invalidated or not. This can be done by extending default JwtSecurityTokenHandler and you should be able to call Redis from there.

In ConfigureServices add:

services.AddSingleton<IConnectionMultiplexer>(ConnectionMultiplexer.Connect("yourConnectionString"));
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(opt =>
    {
        opt.SecurityTokenValidators.Clear();
        // or just pass connection multiplexer directly, it's a singleton anyway...
        opt.SecurityTokenValidators.Add(new RevokableJwtSecurityTokenHandler(services.BuildServiceProvider()));
    });

Create your own exception:

public class SecurityTokenRevokedException : SecurityTokenException
{
    public SecurityTokenRevokedException()
    {
    }

    public SecurityTokenRevokedException(string message) : base(message)
    {
    }

    public SecurityTokenRevokedException(string message, Exception innerException) : base(message, innerException)
    {
    }
}

Extend the default handler:

public class RevokableJwtSecurityTokenHandler : JwtSecurityTokenHandler
{
    private readonly IConnectionMultiplexer _redis;

    public RevokableJwtSecurityTokenHandler(IServiceProvider serviceProvider)
    {
        _redis = serviceProvider.GetRequiredService<IConnectionMultiplexer>();
    }

    public override ClaimsPrincipal ValidateToken(string token, TokenValidationParameters validationParameters,
        out SecurityToken validatedToken)
    {
        // make sure everything is valid first to avoid unnecessary calls to DB
        // if it's not valid base.ValidateToken will throw an exception, we don't need to handle it because it's handled here: https://github.com/aspnet/Security/blob/beaa2b443d46ef8adaf5c2a89eb475e1893037c2/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs#L107-L128
        // we have to throw our own exception if the token is revoked, it will cause validation to fail
        var claimsPrincipal = base.ValidateToken(token, validationParameters, out validatedToken); 
        var claim = claimsPrincipal.FindFirst(JwtRegisteredClaimNames.Jti);
        if (claim != null && claim.ValueType == ClaimValueTypes.String)
        {
            var db = _redis.GetDatabase();
            if (db.KeyExists(claim.Value)) // it's blacklisted! throw the exception
            {
                // there's a bunch of built-in token validation codes: https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/7692d12e49a947f68a44cd3abc040d0c241376e6/src/Microsoft.IdentityModel.Tokens/LogMessages.cs
                // but none of them is suitable for this
                throw LogHelper.LogExceptionMessage(new SecurityTokenRevokedException(LogHelper.FormatInvariant("The token has been revoked, securitytoken: '{0}'.", validatedToken)));
            }
        }

        return claimsPrincipal;
    }
}

Then on your password change or whatever set the key with jti of the token to invalidate it.

Limitation!: all methods in JwtSecurityTokenHandler are synchronous, this is bad if you want to have some IO-bound calls and ideally, you would use await db.KeyExistsAsync(claim.Value) there. The issue for this is tracked here: https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/468 unfortunately no updates for this since 2016 :(

It's funny because the function where token is validated is async: https://github.com/aspnet/Security/blob/beaa2b443d46ef8adaf5c2a89eb475e1893037c2/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs#L107-L128

A temporary workaround would be to extend JwtBearerHandler and replace the implementation of HandleAuthenticateAsync with override without calling the base so it would call your async version of validate. And then use this logic to add it.

The most recommended and actively maintained Redis clients for C#:

Might help you to choose one: Difference between StackExchange.Redis and ServiceStack.Redis

StackExchange.Redis has no limitations and is under the MIT license.

So I would go with the StackExchange's one

Solution 2

The simplest way would be: Signing the JWT with the users current password hash which guarantees single-usage of every issued token. This is because the password hash always changes after successful password-reset.

There is no way the same token can pass verification twice. The signature check would always fail. The JWT's we issue become single-use tokens.

Source- https://www.jbspeakr.cc/howto-single-use-jwt/

Solution 3

The following approach brings together the best of each approach proposed previously:

  1. Create the column "password_id" in the "user" table.
  2. Assign a new UUID to "password_id" when creating a user.
  3. Assign a new UUID to "password_id" every time the user changes his password.
  4. Sign the authorization JWTs using the "password_id" of the respective user.
  5. If more performance is needed, simply store the "password_id" of the users in Redis.

Advantages of this approach:

  • If a user changes his password all JWTs existing up to that moment will automatically become invalid forever.
  • It does not matter if a user changes his password to an old one.
  • It is not necessary to store the JWTs in the server side.
  • It is not necessary to add any extra data in the JWT payload.
  • The implementation using Redis is very simple.
Share:
10,604
Dante R.
Author by

Dante R.

IT enthusiast. I'm trying to learn everything that i can about programming &amp; computer world in general.

Updated on July 22, 2022

Comments

  • Dante R.
    Dante R. almost 2 years

    I am working on an API that uses JWT token auth. I've created some logic behind it to change user password with a verification code & such.

    Everything works, passwords get changed. But here's the catch: Even if the user password has changed and i get a new JWT token when authenticating...the old token still works.

    Any tip on how i could refresh/invalidate tokens after a password change?

    EDIT: I've got an idea on how to do it since i've heard you can't actually invalidate JWT tokens. My idea would be to create a new user column which has something like "accessCode" and store that access code in the token. Whenever i change the password i also change accessCode (something like 6 digit random number) and i implement a check for that accessCode when doing API calls (if the accesscode used in the token doesnt match the one in the db -> return unauthorized).

    Do you guys think that would be a good approach or is there some other way ?

  • Alessandro R
    Alessandro R over 5 years
    Redis is a great idea for storing tokens and black list them.
  • Dante R.
    Dante R. over 5 years
    I have 0 knowledge about Redis to be honest so i've got 2 questions: 1 - With redis i'm not hitting my DB on every request, but isnt it the same since i'm hitting the Redis DB ? 2 - do you perhaps have any good documentation regarding Redis or should i just check the official docs for more info?
  • Konrad
    Konrad over 5 years
    1. You will be hitting Redis DB on every request, but redis is a key-value store and it should be very fast as compared to hitting regular relational DB or whatever you use 2. You will have to find C# client that satisfies you redis.io/clients the documentation for redis in general is here: redis.io/documentation
  • Konrad
    Konrad over 5 years
    You can use "jti" (JWT ID) to uniquely identify the token and use it as a key.
  • Konrad
    Konrad over 5 years
    "Redis’ performance is made up of two key factors: throughput and latency" - blog.newrelic.com/product-news/redis-performance-metrics
  • Konrad
    Konrad over 5 years
    When I'll have time I'll create sample app with token invalidation using redis and update the answer with more details regarding implementation.
  • Konrad
    Konrad over 5 years
    @DanteR. yeah, you need to figure in which override exactly you could put this logic, don't forget to call the base. Unfortunately, Microsoft didn't take time to document how to do this.
  • Konrad
    Konrad over 5 years
    @DanteR. added more extensive implementation example
  • Dante R.
    Dante R. over 5 years
    @Konrad Thank you sir! I'll give it a try as soon as i set up a redis server.
  • mahfuj asif
    mahfuj asif about 4 years
    Odd case: User might set old password as new, and previous token will be valid again
  • janv
    janv about 4 years
    I think we should be able to add restriction such that the new password should be different than the old password. Throw an error like "Password is same as the old password, please enter a different password". In that case user might stop from resetting password and would login into their account.
  • mahfuj asif
    mahfuj asif about 4 years
    suppose user had password pass1 and created token1 to change password, changed to pass2 , then again goes back to pass1. Thus token1 becomes valid again
  • janv
    janv about 4 years
    I am not sure why an user would do all of this. We also have an expiry time set on JWT token, for password recovery it should be anywhere between 1 hour to 1 day. So even when user manage to revert back to pass1, after we cross the JWT token expiry time it will be invalidated
  • Kunal Mukherjee
    Kunal Mukherjee almost 3 years
    @Konrad this token validation parameters using redis sit on the resource server or the auth server ?
  • Konrad
    Konrad almost 3 years
    @KunalMukherjee Wherever you validate your tokens
  • Kunal Mukherjee
    Kunal Mukherjee almost 3 years
    @Konrad so that means resource server right ?
  • Konrad
    Konrad almost 3 years
    @KunalMukherjee yes