更改密码后如何使令牌失效

时间:2018-09-24 09:30:52

标签: c# jwt asp.net-core-2.0 asp.net-core-webapi

我正在研究使用JWT令牌认证的API。我已经在其背后创建了一些逻辑,以使用验证码等更改用户密码。

一切正常,密码得到更改。但是这里有个陷阱: 即使用户密码已更改并且在身份验证时我得到了新的JWT令牌,旧令牌仍然有效。

关于如何在更改密码后如何刷新/使令牌无效的任何提示?

编辑:由于我听说您实际上无法使JWT令牌无效,所以我对如何执行此操作有一个想法。 我的想法是创建一个具有“ accessCode”之类的新用户列,并将该访问代码存储在令牌中。每当我更改密码时,我也会更改accessCode(类似于6位数的随机数),并且在进行API调用时会执行对该accessCode的检查(如果令牌中使用的访问代码与db中的访问代码不匹配->返回未授权)。

你们认为这是一个好方法还是还有其他方法?

2 个答案:

答案 0 :(得分:5)

最简单的撤销/失效方法可能只是删除客户端上的令牌,并祈祷没有人劫持它并滥用它。

您使用“ accessCode”列的方法行得通,但我会担心性能。

另一种可能是更好的方法是将某些数据库中的令牌列入黑名单。我认为Redis将是最好的选择,因为它通过EXPIRE支持超时,因此您可以将其设置为与JWT令牌相同的值。当令牌过期时,它将自动删除。

您将需要快速的响应时间,因为您将必须检查每个需要授权的请求上的令牌是否仍然有效(不在黑名单或不同的accessCode中),这意味着使用无效的令牌调用数据库每个请求。


刷新令牌不是解决方案

有人建议使用寿命长的刷新令牌和寿命短的访问令牌。您可以将访问令牌设置为在10分钟后失效,并且在更改密码后,该令牌仍将在10分钟内有效,但是它将过期,并且您将必须使用刷新令牌来获取新的访问令牌。就个人而言,我对此表示怀疑,因为刷新令牌也可以被劫持:http://appetere.com/post/how-to-renew-access-tokens,然后您还需要一种使它们无效的方法,因此,最终,您不可避免地将它们存储在某个位置


使用StackExchange.Redis的ASP.NET Core实现

您正在使用ASP.NET Core,因此您将需要找到一种方法来添加自定义JWT验证逻​​辑,以检查令牌是否无效。可以通过extending default JwtSecurityTokenHandler完成,您应该可以从那里致电Redis。

在ConfigureServices中添加:

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()));
    });

创建自己的异常:

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

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

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

扩展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;
    }
}

然后在您更改密码或使用令牌的jti设置密钥以使其无效时进行任何操作。

限制!JwtSecurityTokenHandler中的所有方法都是同步的,如果您想进行一些IO绑定调用,这是很糟糕的,理想情况下,您可以在此处使用await db.KeyExistsAsync(claim.Value)。此处跟踪了此问题:https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/468很遗憾,自2016年以来没有更新:(

这很有趣,因为验证令牌的功能是异步的:https://github.com/aspnet/Security/blob/beaa2b443d46ef8adaf5c2a89eb475e1893037c2/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs#L107-L128

一个临时的解决方法是扩展JwtBearerHandler并用HandleAuthenticateAsync替换override的实现,而无需调用基,因此它将调用您的异步版本的validate。然后使用this logic添加它。

最推荐和最活跃的C#Redis客户端:

可能会帮助您选择一个:Difference between StackExchange.Redis and ServiceStack.Redis

  

StackExchange.Redis没有限制,并且已获得MIT许可。

所以我会选择StackExchange的那个

答案 1 :(得分:1)

最简单的方法是:使用用户当前的密码哈希对JWT进行签名,以保证每个已发行令牌的一次性使用。这是因为成功重置密码后,密码哈希始终会更改。

同一令牌无法两次通过验证。签名检查将始终失败。我们发行的JWT成为一次性令牌。

来源-https://www.raspberrypi.org/forums/viewtopic.php?p=774050