web-dev-qa-db-fra.com

Comment gérer le jeton d'accès expiré dans le noyau asp.net à l'aide du jeton d'actualisation avec OpenId Connect

J'ai configuré un serveur ASOS OpenIdConnect à l'aide et une application mvc principale asp.net qui utilise le "Microsoft.AspNetCore.Authentication.OpenIdConnect": "1.0.0 et" Microsoft.AspNetCore.Authentication.Cookies ":" 1.0.0 ". J'ai testé le workflow "Code d'autorisation" et tout fonctionne.

L'application Web client traite l'authentification comme prévu et crée un cookie stockant id_token, access_token et refresh_token.

Comment forcer Microsoft.AspNetCore.Authentication.OpenIdConnect à demander un nouveau access_token à son expiration?

L'application asp.net core mvc ignore le access_token expiré.

Je voudrais que openidconnect voit le access_token expiré puis passe un appel en utilisant le jeton d'actualisation pour obtenir un nouveau access_token. Il doit également mettre à jour les valeurs des cookies. Si la demande d'actualisation du jeton échoue, je m'attends à ce que openidconnect "déconnecte" le cookie (supprimez-le ou quelque chose).

app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AutomaticAuthenticate = true,
            AutomaticChallenge = true,
            AuthenticationScheme = "Cookies"
        });

app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
        {
            ClientId = "myClient",
            ClientSecret = "secret_secret_secret",
            PostLogoutRedirectUri = "http://localhost:27933/",
            RequireHttpsMetadata = false,
            GetClaimsFromUserInfoEndpoint = true,
            SaveTokens = true,
            ResponseType = OpenIdConnectResponseType.Code,
            AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet,
            Authority = http://localhost:27933,
            MetadataAddress = "http://localhost:27933/connect/config",
            Scope = { "email", "roles", "offline_access" },
        });
13
longday

Il semble qu'il n'y ait pas de programmation dans l'authentification openidconnect pour le noyau asp.net pour gérer le access_token sur le serveur après réception.

J'ai constaté que je peux intercepter l'événement de validation des cookies et vérifier si le jeton d'accès a expiré. Si tel est le cas, effectuez un appel HTTP manuel au point de terminaison du jeton avec le grant_type = refresh_token.

En appelant context.ShouldRenew = true; cela entraînera la mise à jour du cookie et son renvoi au client dans la réponse.

J'ai fourni la base de ce que j'ai fait et je travaillerai pour mettre à jour cette réponse une fois que tout aura été résolu.

app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AutomaticAuthenticate = true,
            AutomaticChallenge = true,
            AuthenticationScheme = "Cookies",
            ExpireTimeSpan = new TimeSpan(0, 0, 20),
            SlidingExpiration = false,
            CookieName = "WebAuth",
            Events = new CookieAuthenticationEvents()
            {
                OnValidatePrincipal = context =>
                {
                    if (context.Properties.Items.ContainsKey(".Token.expires_at"))
                    {
                        var expire = DateTime.Parse(context.Properties.Items[".Token.expires_at"]);
                        if (expire > DateTime.Now) //TODO:change to check expires in next 5 mintues.
                        {
                            logger.Warn($"Access token has expired, user: {context.HttpContext.User.Identity.Name}");

                            //TODO: send refresh token to ASOS. Update tokens in context.Properties.Items
                            //context.Properties.Items["Token.access_token"] = newToken;
                            context.ShouldRenew = true;
                        }
                    }
                    return Task.FromResult(0);
                }
            }
        });
15
longday

Vous devez activer la génération de refresh_token en définissant dans startup.cs:

  • Définition des valeurs sur AuthorizationEndpointPath = "/ connect/authorize"; // nécessaire pour un rafraîchissement
  • Définition des valeurs sur TokenEndpointPath = "/ connect/token"; // nom du point de terminaison du jeton standard

Dans votre fournisseur de jetons, avant de valider la demande de jeton à la fin de la méthode HandleTokenrequest, assurez-vous d'avoir défini la portée hors ligne:

        // Call SetScopes with the list of scopes you want to grant
        // (specify offline_access to issue a refresh token).
        ticket.SetScopes(
            OpenIdConnectConstants.Scopes.Profile,
            OpenIdConnectConstants.Scopes.OfflineAccess);

Si cela est correctement configuré, vous devriez recevoir un refresh_token lorsque vous vous connectez avec un mot de passe grant_type.

Ensuite, de votre client, vous devez émettre la demande suivante (j'utilise Aurelia):

refreshToken() {
    let baseUrl = yourbaseUrl;

    let data = "client_id=" + this.appState.clientId
               + "&grant_type=refresh_token"
               + "&refresh_token=myRefreshToken";

    return this.http.fetch(baseUrl + 'connect/token', {
        method: 'post',
        body : data,
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Accept': 'application/json' 
        }
    });
}

et c'est tout, assurez-vous que votre fournisseur d'authentification dans HandleRequestToken n'essaie pas de manipuler la demande qui est de type refresh_token:

    public override async Task HandleTokenRequest(HandleTokenRequestContext context)
    {
        if (context.Request.IsPasswordGrantType())
        {
            // Password type request processing only
            // code that shall not touch any refresh_token request
        }
        else if(!context.Request.IsRefreshTokenGrantType())
        {
            context.Reject(
                    error: OpenIdConnectConstants.Errors.InvalidGrant,
                    description: "Invalid grant type.");
            return;
        }

        return;
    }

Le refresh_token doit simplement pouvoir passer par cette méthode et est géré par un autre middleware qui gère refresh_token.

Si vous souhaitez une connaissance plus approfondie de ce que fait le serveur d'authentification, vous pouvez consulter le code de OpenIdConnectServerHandler:

https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server/blob/master/src/AspNet.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.Exchange.cs

Côté client, vous devez également être capable de gérer l'actualisation automatique du jeton, voici un exemple d'intercepteur http pour Angular 1.X, où l'on gère 401 réponses, actualisez le jeton , puis réessayez la demande:

'use strict';
app.factory('authInterceptorService',
    ['$q', '$injector', '$location', 'localStorageService',
    function ($q, $injector, $location, localStorageService) {

    var authInterceptorServiceFactory = {};
    var $http;

    var _request = function (config) {

        config.headers = config.headers || {};

        var authData = localStorageService.get('authorizationData');
        if (authData) {
            config.headers.Authorization = 'Bearer ' + authData.token;
        }

        return config;
    };

    var _responseError = function (rejection) {
        var deferred = $q.defer();
        if (rejection.status === 401) {
            var authService = $injector.get('authService');
            console.log("calling authService.refreshToken()");
            authService.refreshToken().then(function (response) {
                console.log("token refreshed, retrying to connect");
                _retryHttpRequest(rejection.config, deferred);
            }, function () {
                console.log("that didn't work, logging out.");
                authService.logOut();

                $location.path('/login');
                deferred.reject(rejection);
            });
        } else {
            deferred.reject(rejection);
        }
        return deferred.promise;
    };

    var _retryHttpRequest = function (config, deferred) {
        console.log('autorefresh');
        $http = $http || $injector.get('$http');
        $http(config).then(function (response) {
            deferred.resolve(response);
        },
        function (response) {
            deferred.reject(response);
        });
    }

    authInterceptorServiceFactory.request = _request;
    authInterceptorServiceFactory.responseError = _responseError;
    authInterceptorServiceFactory.retryHttpRequest = _retryHttpRequest;

    return authInterceptorServiceFactory;
}]);

Et voici un exemple que je viens de faire pour Aurelia, cette fois j'ai enveloppé mon client http dans un gestionnaire http qui vérifie si le jeton a expiré ou non. S'il est expiré, il actualisera d'abord le jeton, puis exécutera la demande. Il utilise une promesse de garder l'interface avec les services de données côté client cohérente. Ce gestionnaire expose la même interface que le client aurelia-fetch.

import {inject} from 'aurelia-framework';
import {HttpClient} from 'aurelia-fetch-client';
import {AuthService} from './authService';

@inject(HttpClient, AuthService)
export class HttpHandler {

    constructor(httpClient, authService) {
        this.http = httpClient;
        this.authService = authService;
    }

    fetch(url, options){
        let _this = this;
        if(this.authService.tokenExpired()){
            console.log("token expired");
            return new Promise(
                function(resolve, reject) {
                    console.log("refreshing");
                    _this.authService.refreshToken()
                    .then(
                       function (response) {
                           console.log("token refreshed");
                        _this.http.fetch(url, options).then(
                            function (success) { 
                                console.log("call success", url);
                                resolve(success);
                            }, 
                            function (error) { 
                                console.log("call failed", url);
                                reject(error); 
                            }); 
                       }, function (error) {
                           console.log("token refresh failed");
                           reject(error);
                    });
                }
            );
        } 
        else {
            // token is not expired, we return the promise from the fetch client
            return this.http.fetch(url, options); 
        }
    }
}

Pour jquery, vous pouvez regarder un jquery oAuth:

https://github.com/esbenp/jquery-oauth

J'espère que cela t'aides.

2
Darxtar

Suite à la réponse de @ longday, j'ai réussi à utiliser ce code pour forcer une actualisation client sans avoir à interroger manuellement un point de terminaison id ouvert:

OnValidatePrincipal = context =>
{
    if (context.Properties.Items.ContainsKey(".Token.expires_at"))
    {
        var expire = DateTime.Parse(context.Properties.Items[".Token.expires_at"]);
        if (expire > DateTime.Now) //TODO:change to check expires in next 5 mintues.
        {
            context.ShouldRenew = true;
            context.RejectPrincipal();
        }
    }

    return Task.FromResult(0);
}
0
Shane