如何使用带有 OpenId Connect 的刷新令牌在 asp.net 核心中处理过期的访问令牌

Posted

技术标签:

【中文标题】如何使用带有 OpenId Connect 的刷新令牌在 asp.net 核心中处理过期的访问令牌【英文标题】:How to handle expired access token in asp.net core using refresh token with OpenId Connect 【发布时间】:2017-02-23 06:39:08 【问题描述】:

我已经使用“Microsoft.AspNetCore.Authentication.OpenIdConnect”配置了一个 ASOS OpenIdConnect 服务器和一个使用“Microsoft.AspNetCore.Authentication.OpenIdConnect”的 asp.net core mvc 应用程序:“1.0.0”和“Microsoft.AspNetCore.Authentication.Cookies”:“1.0. 0”。我已经测试了“授权码”工作流程,一切正常。

客户端 Web 应用程序按预期处理身份验证并创建一个存储 id_token、access_token 和 refresh_token 的 cookie。

如何强制 Microsoft.AspNetCore.Authentication.OpenIdConnect 在新的 access_token 过期时请求它?

asp.net core mvc 应用忽略过期的 access_token。

我想让 openidconnect 查看过期的 access_token,然后使用刷新令牌进行调用以获取新的 access_token。它还应该更新 cookie 值。如果刷新令牌请求失败,我希望 openidconnect “退出” cookie(删除它或其他东西)。

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" ,
        );

【问题讨论】:

【参考方案1】:

asp.net core的openidconnect认证好像没有编程来管理服务器上收到的access_token。

我发现我可以拦截cookie验证事件并检查访问令牌是否已过期。如果是这样,请使用 grant_type=refresh_token 对令牌端点进行手动 HTTP 调用。

通过调用 context.ShouldRenew = true;这将导致 cookie 被更新并在响应中发送回客户端。

我已经提供了我所做工作的基础,一旦所有工作得到解决,我将努力更新这个答案。

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

【讨论】:

@longday FWIW,这也是我实现它的方式。您应该将您的答案标记为已接受的答案;) 你真的应该在 OnValidatePrincipal 中返回一个返回 Task.FromResult(0) 吗?为什么?可以使用 Task.CompletedTask 代替吗? 应该是expire < DateTime.Now。此外,如果支持,可以使用Task.CompletedTask 感谢您的帖子,@longday。现在我在 asp.net 项目中实现您的解决方案时遇到问题 - 而不是 asp.net 核心。在 asp.net CookieAuthenticationOptions 中,没有名为 Events 的属性。你能告诉我如何解决这个问题吗?再次感谢。 @Harry,在经典的 ASP.net 中,CookieAuthenticationOptions 具有 Provider 属性。通过实现一个继承自 CookieAuthenticationProvider 的类并覆盖其中一个方法,您可以执行与上面显示的事件非常相似的事情。【参考方案2】:

您必须通过在startup.cs中设置来启用refresh_token的生成:

将值设置为 AuthorizationEndpointPath = "/connect/authorize"; // 需要刷新令牌 将值设置为 TokenEndpointPath = "/connect/token"; // 标准令牌端点名称

在您的令牌提供程序中,在 HandleTokenrequest 方法末尾验证令牌请求之前,请确保您已设置离线范围:

        // 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);

如果设置正确,当您使用密码 grant_type 登录时,您应该会收到一个 refresh_token。

那么您必须从您的客户发出以下请求(我正在使用 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' 
        
    );

就是这样,请确保您在 HandleRequestToken 中的身份验证提供程序没有试图操纵类型为 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;
    

refresh_token 应该只是能够通过这个方法,并由另一个处理 refresh_token 的中间件处理。

如果您想更深入地了解身份验证服务器在做什么,可以查看 OpenIdConnectServerHandler 的代码:

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

在客户端,您还必须能够处理令牌的自动刷新,这是 Angular 1.X 的 http 拦截器示例,其中一个处理 401 响应,刷新令牌,然后重试请求:

'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;
]);

这是我刚刚为 Aurelia 做的一个示例,这次我将我的 http 客户端包装到一个 http 处理程序中,该处理程序检查令牌是否过期。如果它已过期,它将首先刷新令牌,然后执行请求。它使用承诺来保持与客户端数据服务的接口一致。此处理程序公开与 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); 
        
    

对于 jquery,您可以查看 jquery oAuth:

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

希望这会有所帮助。

【讨论】:

如何让客户端通过使用 refresh_token 请求新令牌来自动处理过期的 access_token?我正在使用客户端库“Microsoft.AspNetCore.Authentication.OpenIdConnect”:“1.0.0。如果我必须手动处理刷新令牌,最好的方法是什么?如何更新客户端 cookie? 实现取决于您的前端框架...我使用的是 OAuth 2 令牌,所以我实际上在将令牌发送回客户端之前自己将令牌设置在 cookie 中...如果 cookie 具有相同的命名它将被新的替换。您有 2 种自动刷新的方法:1) 预防性,在执行实际请求之前检查令牌的到期时间并刷新 2) 反应性:您在客户端中侦听 http 状态,如果您收到 401,您刷新令牌然后重试请求。如果你使用 Angular 或 Aurelia,你可以配置一个 http 拦截器 我在 Angular 1.X 的答案中添加了一个示例,希望对您有所帮助。【参考方案3】:

根据@longday 的回答,我已经成功地使用此代码强制客户端刷新,而无需手动查询开放 id 端点:

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

【讨论】:

以上是关于如何使用带有 OpenId Connect 的刷新令牌在 asp.net 核心中处理过期的访问令牌的主要内容,如果未能解决你的问题,请参考以下文章

OpenID Connect:不同身份提供者之间的刷新令牌行为不一致

使用带有 OpenID Connect 提供程序的 spring-security-oauth2 客户端时如何访问“id_token”和“refresh_token”?

Azure AD B2C OpenID Connect 刷新令牌

带有刷新令牌的 Keycloak 客户端凭据授予类型

OAuth 和 OpenId Connect 令牌说明

如何使用 openid connect OIDC 加密 REST 正文