web-dev-qa-db-fra.com

Gestion des jetons de rafraîchissement expirés dans ASP.NET Core

VOIR ci-dessous pour le code qui a résolu ce problème

J'essaie de trouver le moyen le plus efficace et le plus efficace de gérer un jeton d'actualisation qui a expiré dans ASP.NET Core 2.1.

Laissez-moi vous expliquer un peu plus.

J'utilise OAUTH2 et OIDC pour demander des flux d'octroi de code d'autorisation (ou flux hybride avec OIDC). Ce type de flux/octroi me donne accès à un AccessToken et à un RefreshToken (code d'autorisation également, mais ce n'est pas pour cette question).

Le jeton d'accès et le jeton d'actualisation sont stockés par le noyau ASP.NET et peuvent être récupérés à l'aide de HttpContext.GetTokenAsync("access_token"); et HttpContext.GetTokenAsync("refresh_token"); respectivement.

Je peux rafraîchir le access_token sans aucun problème. Le problème entre en jeu lorsque le refresh_token est expiré, révoqué ou invalide d'une manière ou d'une autre.

Le flux correct consisterait à ce que l'utilisateur se reconnecte et revienne dans le flux d'authentification à nouveau. Ensuite, l'application reçoit un nouvel ensemble de jetons.

Ma question est de savoir comment y parvenir de la manière la meilleure et la plus correcte. J'ai décidé d'écrire un middleware personnalisé qui tente de renouveler le access_token s'il a expiré. Le middleware définit ensuite le nouveau jeton dans le AuthenticationProperties pour le HttpContext afin qu'il puisse être utilisé par tous les appels plus tard dans le canal.

Si l'actualisation du jeton échoue pour une raison quelconque, je dois appeler à nouveau ChallengeAsync. J'appelle ChallengeAsync à partir du middleware.

C'est là que je rencontre un comportement intéressant. La plupart du temps, cela fonctionne, cependant, parfois, j'obtiens 500 erreurs sans aucune information utile sur ce qui échoue. Il semble presque que le middleware rencontre des problèmes en essayant d'appeler ChallengeAsync à partir du middleware, et peut-être qu'un autre middleware essaie également d'accéder au contexte.

Je ne sais pas trop ce qui se passe. Je ne sais pas trop si c'est le bon endroit pour mettre cette logique ou non. Peut-être que je ne devrais pas avoir cela dans le middleware, peut-être ailleurs. Peut-être que Polly pour le HttpClient est le meilleur endroit.

Je suis ouvert à toutes les idées.

Merci pour toute l'aide que vous pouvez apporter.

Solution de code qui a fonctionné pour moi


Merci à Mickaël Derriey pour l'aide et la direction (assurez-vous de voir sa réponse pour plus d'informations sur le contexte de cette résolution) Voici la solution que j'ai trouvée et qui fonctionne pour moi:

options.Events = new CookieAuthenticationEvents
{
    OnValidatePrincipal = context =>
    {
        //check to see if user is authenticated first
        if (context.Principal.Identity.IsAuthenticated)
        {
            //get the users tokens
            var tokens = context.Properties.GetTokens();
            var refreshToken = tokens.FirstOrDefault(t => t.Name == "refresh_token");
            var accessToken = tokens.FirstOrDefault(t => t.Name == "access_token");
            var exp = tokens.FirstOrDefault(t => t.Name == "expires_at");
            var expires = DateTime.Parse(exp.Value);
            //check to see if the token has expired
            if (expires < DateTime.Now)
            {
                //token is expired, let's attempt to renew
                var tokenEndpoint = "https://token.endpoint.server";
                var tokenClient = new TokenClient(tokenEndpoint, clientId, clientSecret);
                var tokenResponse = tokenClient.RequestRefreshTokenAsync(refreshToken.Value).Result;
                //check for error while renewing - any error will trigger a new login.
                if (tokenResponse.IsError)
                {
                    //reject Principal
                    context.RejectPrincipal();
                    return Task.CompletedTask;
                }
                //set new token values
                refreshToken.Value = tokenResponse.RefreshToken;
                accessToken.Value = tokenResponse.AccessToken;
                //set new expiration date
                var newExpires = DateTime.UtcNow + TimeSpan.FromSeconds(tokenResponse.ExpiresIn);
                exp.Value = newExpires.ToString("o", CultureInfo.InvariantCulture);
                //set tokens in auth properties 
                context.Properties.StoreTokens(tokens);
                //trigger context to renew cookie with new token values
                context.ShouldRenew = true;
                return Task.CompletedTask;
            }
        }
        return Task.CompletedTask;
    }
};
12
bugnuker

Le jeton d'accès et le jeton d'actualisation sont stockés par le noyau ASP.NET

Je pense qu'il est important de noter que les jetons sont stockés dans le cookie qui identifie l'utilisateur de votre application.

Maintenant, c'est mon opinion, mais je ne pense pas qu'un middleware personnalisé soit le bon endroit pour rafraîchir les jetons. La raison en est que si vous actualisez correctement le jeton, vous devrez remplacer celui existant et le renvoyer au navigateur, sous la forme d'un nouveau cookie qui remplacera celui existant.

C'est pourquoi je pense que l'endroit le plus pertinent pour le faire est lorsque le cookie est lu par ASP.NET Core. Chaque mécanisme d'authentification expose plusieurs événements; pour les cookies, il y en a un appelé ValidatePrincipal qui est appelé à chaque demande après que le cookie a été lu et qu'une identité en a été correctement désérialisée.

public void ConfigureServices(ServiceCollection services)
{
    services
        .AddAuthentication()
        .AddCookies(new CookieAuthenticationOptions
        {
            Events = new CookieAuthenticationEvents
            {
                OnValidatePrincipal = context =>
                {
                    // context.Principal gives you access to the logged-in user
                    // context.Properties.GetTokens() gives you access to all the tokens

                    return Task.CompletedTask;
                }
            }
        });
}

La bonne chose à propos de cette approche est que si vous parvenez à renouveler le jeton et à le stocker dans la AuthenticationProperties, la variable context qui est de type CookieValidatePrincipalContext, possède une propriété appelée ShouldRenew . La définition de cette propriété sur true indique au middleware d'émettre un nouveau cookie.

Si vous ne pouvez pas renouveler le jeton ou si vous trouvez que le jeton d'actualisation est expiré et que vous souhaitez empêcher l'utilisateur d'aller de l'avant, cette même classe a une méthode RejectPrincipal qui demande au cookie middleware pour traiter la demande comme si elle était anonyme.

La bonne chose à ce sujet est que si votre application MVC permet uniquement aux utilisateurs authentifiés d'y accéder, MVC se chargera d'émettre le HTTP 401 réponse que le système d'authentification interceptera et transformera en défi et l'utilisateur sera redirigé vers le fournisseur d'identité.

J'ai du code qui montre comment cela fonctionnerait au mderriey/TokenRenewal référentiel sur GitHub. Bien que l'intention soit différente, elle montre la mécanique de l'utilisation de ces événements.

10
Mickaël Derriey