web-dev-qa-db-ja.com

パスワード変更後にトークンを無効にする方法

JWTトークン認証を使用するAPIに取り組んでいます。確認コードなどでユーザーパスワードを変更するためのロジックを作成しました。

すべてが機能し、パスワードが変更されます。しかし、ここに問題があります。ユーザーパスワードが変更され、認証時に新しいJWTトークンを取得した場合でも、古いトークンは引き続き機能します。

パスワード変更後にトークンを更新/無効化する方法に関するヒントはありますか?

編集:私はあなたが実際にJWTトークンを無効にすることができないと聞いたので、それを行う方法についてのアイデアを持っています。私のアイデアは、 "accessCode"のようなものを持つ新しいユーザー列を作成し、そのアクセスコードをトークンに格納することです。パスワードを変更するたびに、accessCode(6桁の乱数など)も変更し、API呼び出しを実行するときにそのaccessCodeのチェックを実装します(トークンで使用されているアクセスコードがdbのアクセスコードと一致しない場合->未承認を返します)。

それは良いアプローチだと思いますか、それとも他の方法がありますか?

8
Dante R.

取り消し/無効化する最も簡単な方法は、おそらくクライアントのトークンを削除し、誰もそれをハイジャックして悪用しないように祈ることです。

「accessCode」列を使用したアプローチは機能しますが、パフォーマンスが心配です。

他の、おそらくより良い方法は、いくつかのデータベースでトークンをブラックリストに載せることです。 EXPIREを介してタイムアウトをサポートしているため、JWTトークンにあるのと同じ値に設定するだけでよいので、Redisがこれに最適だと思います。トークンの有効期限が切れると、自動的に削除されます。

承認が必要な各リクエストでトークンがまだ有効であるか(ブラックリストや別の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)
    {
    }
}
_

拡張 デフォルトのハンドラ

_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で置き換えることにより、非同期バージョンの検証を呼び出すことです。次に、 this logic を使用して追加します。

C#で最も推奨され、積極的にメンテナンスされているRedisクライアント:

StackExchange.RedisとServiceStack.Redisの違い

StackExchange.Redisには制限がなく、MITライセンスの下にあります。

だから私はStackExchangeのものを使います

9
Konrad

最も簡単な方法は次のとおりです。発行されたすべてのトークンの単一使用を保証するユーザーの現在のパスワードハッシュでJWTに署名します。これは、パスワードのリセットが成功すると、パスワードハッシュが常に変更されるためです。

同じトークンが検証を2回通過することはできません。署名チェックは常に失敗します。発行するJWTは使い捨てトークンになります。

ソース- https://www.jbspeakr.cc/howto-single-use-jwt/

4
janv