web-dev-qa-db-fra.com

Comment invalider des jetons après un changement de mot de passe

Je travaille sur une API qui utilise l'authentification par jeton JWT. J'ai créé une logique derrière cela pour changer le mot de passe de l'utilisateur avec un code de vérification, etc.

Tout fonctionne, les mots de passe sont modifiés. Mais voici le problème: même si le mot de passe utilisateur a changé et que j'obtiens un nouveau jeton JWT lors de l'authentification ... l'ancien jeton fonctionne toujours.

Une astuce sur la façon dont je pourrais actualiser/invalider les jetons après un changement de mot de passe?

EDIT: J'ai une idée sur la façon de le faire depuis que j'ai entendu dire que vous ne pouvez pas réellement invalider les jetons JWT. Mon idée serait de créer une nouvelle colonne utilisateur qui a quelque chose comme "accessCode" et de stocker ce code d'accès dans le jeton. Chaque fois que je change le mot de passe, je change également accessCode (quelque chose comme un nombre aléatoire à 6 chiffres) et j'implémente une vérification de ce accessCode lors des appels d'API (si le code d'accès utilisé dans le jeton ne correspond pas à celui dans la base de données -> retour non autorisé).

Pensez-vous que ce serait une bonne approche ou y a-t-il une autre façon?

8
Dante R.

Le moyen le plus simple de révoquer/invalider est probablement de simplement retirer le jeton sur le client et de prier que personne ne le détourne et n'en abuse.

Votre approche avec la colonne "accessCode" fonctionnerait mais je serais inquiet pour les performances.

L'autre et probablement le meilleur moyen serait de mettre la liste noire des jetons dans une base de données. Je pense que Redis serait le meilleur pour cela car il prend en charge les délais d'expiration via EXPIRE donc vous pouvez simplement le définir à la même valeur que celle que vous avez dans votre jeton JWT. Et lorsque le jeton expire, il sera automatiquement supprimé.

Vous aurez besoin d'un temps de réponse rapide pour cela, car vous devrez vérifier si le jeton est toujours valide (pas dans la liste noire ou dans un code d'accès différent) à chaque demande qui nécessite une autorisation et cela signifie appeler votre base de données avec des jetons invalides à chaque demande.


Les jetons d'actualisation ne sont pas la solution

Certaines personnes recommandent d'utiliser des jetons d'actualisation longue durée et des jetons d'accès de courte durée. Vous pouvez définir le jeton d'accès pour, disons, expirer dans 10 minutes et lorsque le mot de passe change, le jeton sera toujours valide pendant 10 minutes, mais il expirera et vous devrez utiliser le jeton d'actualisation pour acquérir le nouveau jeton d'accès. Personnellement, je suis un peu sceptique à ce sujet car le jeton d'actualisation peut également être détourné: http: //appetere.com/post/how-to-renew-access-tokens et puis vous aurez besoin d'un moyen de les invalider ainsi donc, à la fin, vous ne pouvez pas éviter de les stocker quelque part.


Implémentation d'ASP.NET Core à l'aide de StackExchange.Redis

Vous utilisez ASP.NET Core, vous devrez donc trouver un moyen d'ajouter une logique de validation JWT personnalisée pour vérifier si le jeton a été invalidé ou non. Cela peut être fait par étendant la valeur par défaut JwtSecurityTokenHandler et vous devriez pouvoir appeler Redis à partir de là.

Dans ConfigureServices, ajoutez:

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

Créez votre propre exception:

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

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

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

Étendez le gestionnaire par défaut :

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

Ensuite, changez votre mot de passe ou définissez la clé avec jti du jeton pour l'invalider.

Limitation!: toutes les méthodes dans JwtSecurityTokenHandler sont synchrones, c'est mauvais si vous voulez avoir des appels liés aux IO et idéalement, vous utiliserait await db.KeyExistsAsync(claim.Value) là-bas. Le problème est suivi ici: https: //github.com/AzureAD/Azure-activedirectory-identitymodel-extensions-for-dotnet/issues/468 malheureusement aucune mise à jour pour cela depuis 2016 :(

C'est drôle parce que la fonction où le jeton est validé est asynchrone: https: //github.com/aspnet/Security/blob/beaa2b443d46ef8adaf5c2a89eb475e1893037c2/src/Microsoft.AspNetCore.Authentication.JwtBearerL10 L128

Une solution de contournement temporaire serait d'étendre JwtBearerHandler et de remplacer l'implémentation de HandleAuthenticateAsync par override sans appeler la base afin d'appeler votre version asynchrone de validate. Et puis utilisez cette logique pour l'ajouter.

Les clients Redis les plus recommandés et les plus activement entretenus pour C #:

Pourrait vous aider à en choisir un: Différence entre StackExchange.Redis et ServiceStack.Redis

StackExchange.Redis n'a aucune limitation et est sous la licence MIT.

J'irais donc avec celui de StackExchange

9
Konrad

La façon la plus simple serait: de signer le JWT avec le hachage de mot de passe actuel de l'utilisateur, ce qui garantit une utilisation unique de chaque jeton émis. En effet, le hachage du mot de passe change toujours après une réinitialisation réussie du mot de passe.

Il est impossible que le même jeton puisse passer la vérification deux fois. Le contrôle de signature échouera toujours. Les JWT que nous émettons deviennent des jetons à usage unique.

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

4
janv