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 ?
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/
The following approach brings together the best of each approach proposed previously:
Advantages of this approach:
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.
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With