在 ASPMVC 应用程序中使用 OAuth2 刷新令牌

Using OAuth2 refresh tokens in an ASPMVC application

场景

我正在使用 OWIN cookie 身份验证中间件来保护我的网站,如下所示

public void ConfigureAuth(IAppBuilder app)
{
   app.UseCookieAuthentication(new CookieAuthenticationOptions
   {
      AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
      LoginPath = new PathString("/Account/Login"),
      ExpireTimeSpan = new TimeSpan(0, 20, 0),
      SlidingExpiration = true
   });
}

登录时,我使用资源所有者密码流来调用我的令牌服务并检索访问令牌和刷新令牌。

然后我将刷新令牌、访问令牌和访问令牌过期时间添加到我的声明中,然后调用以下命令将此信息保存到我的身份验证 cookie 中。

HttpContext .GetOwinContext() 。验证 .SignIn(claimsIdentityWithTokenAndExpiresAtClaim);

然后在调用任何服务之前,我可以从我当前的声明中检索访问令牌并将其与服务调用相关联。

问题

在调用任何服务之前,我确实应该检查访问令牌是否已过期,如果已过期,请使用刷新令牌获取新令牌。一旦我有了新的访问令牌,我就可以调用该服务,但是我需要使用新的访问令牌、刷新令牌和到期时间来保存一个新的身份验证 cookie。

有什么好的方法可以对服务的调用者透明地执行此操作吗?

尝试过的解决方案

1) 调用每个服务前检查

[Authorize]
public async Task<ActionResult> CallService(ClaimsIdentity claimsIdentity)
{
    var accessToken = GetAccessToken();
    var service = new Service(accessToken).DoSomething();
}

private string GetAccessToken(ClaimsIdentity claimsIdentity) {

    if (claimsIdentity.HasAccessTokenExpired())
    {
        // call sts, get new tokens, create new identity with tokens
        var newClaimsIdentity = ...

        HttpContext
            .GetOwinContext()
            .Authentication
            .SignIn(newClaimsIdentity);

        return newClaimsIdentity;

    } else {
        return claimsIdentity.AccessToken();
    }
}

这可行,但不可持续。此外,我不能再使用依赖注入来注入我的服务,因为该服务在调用时需要访问令牌,而不是构建时。

2) 使用某种服务工厂

在使用其访问令牌创建服务之前,它会根据需要执行刷新。问题是我不确定如何让工厂 return 既提供服务又在实现中以一种很好的方式设置 cookie。

3) 改为在操作过滤器中执行。

想法是会话 cookie 有 20 分钟的滑动到期时间。在每个页面请求中,我可以检查访问令牌是否超过其过期的一半(即,如果访问令牌的有效期为一小时,请检查它是否有不到 30 分钟的有效期)。如果是,请执行刷新。服务可以依赖未过期的访问令牌。假设您在 30 分钟到期前点击了页面并在页面上停留了 30 分钟,假设会话超时(空闲 20 分钟)将在您调用该服务之前启动并且您将被注销。

4) 不执行任何操作并捕获使用过期令牌调用服务的异常

我想不出一个好的方法来获取新令牌并再次重试服务调用而不必担心副作用等。另外,最好先检查过期,而不是等待服务失败所需的时间。

这些解决方案都不是特别优雅。其他人如何处理?

更新:

我花了一些时间研究如何使用您当前的设置在服务器端有效地实现这一点的各种选项。

有多种方法(如 Custom-Middleware、AuthenticationFilter、AuthorizationFilter 或 ActionFilter)可以在服务器端实现此目的。但是,看看这些选项,我会倾向于 AuthroziationFilter。原因是:

  1. AuthroziationFilters 在 AuthenticationFilters 之后执行。因此,您可以在管道的早期根据到期时间决定是否获取新令牌。此外,我们可以确定用户已通过身份验证。

  2. 我们正在处理的场景是关于access_token,它与授权相关而不是认证。

  3. 有了过滤器,我们的优势在于可以选择性地将它与使用该过滤器明确修饰的操作一起使用,这与每个请求都会执行的自定义中间件不同。这很有用,因为在某些情况下,您不希望在不调用任何服务时获得刷新的令牌(因为当前令牌仍然有效,因为我们在过期之前就获得了新令牌)。

  4. Actionfilters 在管道中被调用的时间很晚,我们也没有在动作过滤器中执行方法之后的情况。

这是来自 Whosebug 的 question,其中有一些关于如何使用依赖项注入实现 AuthorizationFilter 的详细信息。

即将向服务附加授权 header:

这发生在您的操作方法中。到这个时候你确定令牌是有效的。因此,我将创建一个抽象基础 class 来实例化 HttpClient class 并设置授权 header。服务 class 实现该基础 class 并使用 HttpClient 调用 Web 服务。这种方法很干净,因为您的设置的消费者不必知道您如何以及何时获取令牌并将其附加到 Web 服务的传出请求。此外,只有在调用 Web 服务时,您才会获取并附加刷新的 access_token。

这是一些示例代码(请注意,我还没有完全测试这段代码,这是为了让您了解如何实现):

public class MyAuthorizeAttribute : FilterAttribute, IAuthorizationFilter
    {
        private const string AuthTokenKey = "Authorization";

        public void OnAuthorization(AuthorizationContext filterContext)
        {
            var accessToken = string.Empty;
            var bearerToken = filterContext.HttpContext.Request.Headers[AuthTokenKey];

            if (!string.IsNullOrWhiteSpace(bearerToken) && bearerToken.Trim().Length > 7)
            {
                accessToken = bearerToken.StartsWith("Bearer ") ? bearerToken.Substring(7) : bearerToken;
            }

            if (string.IsNullOrWhiteSpace(accessToken))
            {
                // Handle unauthorized result Unauthorized!
                filterContext.Result = new HttpUnauthorizedResult();
            }

            // call sts, get new token based on the expiration time. The grace time before which you want to
            //get new token can be based on your requirement. assign it to accessToken


            //Remove the existing token and re-add it
            filterContext.HttpContext.Request.Headers.Remove(AuthTokenKey);
            filterContext.HttpContext.Request.Headers[AuthTokenKey] = $"Bearer {accessToken}";
        }
    }


    public abstract class ServiceBase
    {
        protected readonly HttpClient Client;

        protected ServiceBase()
        {
            var accessToken = HttpContext.Current.Request.Headers["Authorization"];
            Client = new HttpClient();
            Client.DefaultRequestHeaders.Add("Authorization", accessToken);
        }
    }

    public class Service : ServiceBase
    {
        public async Task<string> TestGet()
        {
            return await Client.GetStringAsync("www.google.com");
        }
    }

    public class TestController : Controller
    {
        [Authorize]
        public async Task<ActionResult> CallService()
        {
            var service = new Service();
            var testData = await service.TestGet();
            return Content(testData);
        }
    }

请注意,使用 OAuth 2.0 规范中的客户端凭证流是我们在调用 API 时需要采用的方法。此外,JavaScript 解决方案对我来说感觉更优雅。但是,我相信您的要求可能会迫使您按照自己的方式去做。如果您有任何问题或评论,请告诉我。谢谢。


在声明中添加访问令牌、刷新令牌和过期时间并将其传递给以下服务可能不是一个好的解决方案。声明更适合标识用户信息/授权信息。此外,OpenId 规范指定访问令牌应仅作为授权 header 的一部分发送。我们应该以不同的方式处理 expired/expiring token 的问题。

在客户端,您可以使用这个很棒的 Javascript 库 oidc-client. Now you send this new and valid access token as part of your headers to the server and the server will pass it to the following APIs. As a precaution, you can use the same library to validate the expiration time of the token before sending it to the server. This is much cleaner and better solution in my opinion. There are options to silently update the token without the user noticing it. The library uses a an iframe under the hood to update the token. Here is a link for a video in which the author of the library Brock Allen explains the same concepts. The implementation of this functionality is very straightforward. Examples of how the library can be used is here 在新访问令牌过期之前自动完成获取新访问令牌的过程。我们感兴趣的 JS 调用如下所示:

var settings = {
    authority: 'http://localhost:5000/oidc',
    client_id: 'js.tokenmanager',
    redirect_uri: 'http://localhost:5000/user-manager-sample.html',
    post_logout_redirect_uri: 'http://localhost:5000/user-manager-sample.html',
    response_type: 'id_token token',
    scope: 'openid email roles',

    popup_redirect_uri:'http://localhost:5000/user-manager-sample-popup.html',

    silent_redirect_uri:'http://localhost:5000/user-manager-sample-silent.html',
    automaticSilentRenew:true,

    filterProtocolClaims: true,
    loadUserInfo: true
};
var mgr = new Oidc.UserManager(settings);


function iframeSignin() {
    mgr.signinSilent({data:'some data'}).then(function(user) {
        log("signed in", user);
    }).catch(function(err) {
        log(err);
    });
}

管理器是

的实例

仅供参考,我们可以通过构建自定义中间件并将其用作 MessageHandler 中请求流的一部分来在服务器上实现类似的功能。请让我知道,如果你有任何问题。

谢谢, 索玛。