如何使用OpenId Connect

时间:2016-10-13 23:55:01

标签: cookies asp.net-core openid-connect aspnet-contrib

我已经使用asp.net核心mvc应用程序配置了ASOS OpenIdConnect服务器,该应用程序使用“Microsoft.AspNetCore.Authentication.OpenIdConnect”:“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核心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" },
        });

3 个答案:

答案 0 :(得分:15)

似乎没有编程在openidconnect身份验证中,asp.net核心在收到服务器后管理服务器上的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);
                }
            }
        });

答案 1 :(得分: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中的auth提供程序没有尝试操作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的中间件处理。

如果您想更深入地了解auth服务器正在做什么,您可以查看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处理程序中,检查令牌是否已过期。如果它已过期,它将首先刷新令牌,然后执行请求。它使用promise来保持与客户端数据服务的接口一致。此处理程序公开与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

希望这有帮助。

答案 2 :(得分:1)

继@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);
}