【问题标题】:Using OAuth2 refresh tokens in an ASPMVC application在 ASPMVC 应用程序中使用 OAuth2 刷新令牌
【发布时间】:2016-09-19 02:12:33
【问题描述】:

场景

我正在使用 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) 使用某种服务工厂

在使用其访问令牌创建服务之前,它会在需要时执行刷新。问题在于我不确定如何让工厂返回服务并以一种很好的方式在实现中设置 cookie。

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

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

4) 什么都不做,并在使用过期令牌调用服务时捕获异常

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

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

【问题讨论】:

  • 您对我的回答有任何疑问或问题吗?
  • 我对 MVC 的解决方案更感兴趣。我很想看看自定义中间件的实现。但我的问题实际上是关于 a)您在哪个时间点检查访问令牌过期和 b)您如何透明地将访问令牌传递给需要它的服务(以便他们可以添加到他们的授权标头)。我还建议将访问令牌、刷新令牌和过期令牌存储在身份验证 cookie 中。我并不是建议将它们作为声明传递给服务。
  • 请看我更新的答案。我花了一些时间研究什么是服务器端的最佳方法。请让我知道,如果你有任何问题。谢谢。

标签: oauth-2.0 access-token


【解决方案1】:

更新:

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

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

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

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

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

  4. 动作过滤器在流水线中被调用的时间太晚了,我们也没有在动作过滤器中执行方法之后的情况。

这是来自 Stackoverflow 的 question,其中包含一些关于如何使用依赖注入实现 AuthorizationFilter 的很好的细节。

将授权标头附加到服务:

这发生在您的操作方法中。此时,您可以确定令牌是有效的。因此,我将创建一个抽象基类来实例化一个 HttpClient 类并设置授权标头。服务类实现该基类并使用 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 解决方案对我来说感觉更优雅。但是,我确信您的要求可能会迫使您按照自己的方式进行操作。如果您对 cme​​ts 有任何疑问,请告诉我。谢谢。


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

在客户端,您可以使用这个出色的 Javascript 库 oidc-client 在过期之前自动获取新访问令牌的过程。现在,您将这个新的有效访问令牌作为标头的一部分发送到服务器,服务器会将其传递给以下 API。作为预防措施,您可以在将令牌发送到服务器之前使用相同的库来验证令牌的到期时间。在我看来,这是更清洁、更好的解决方案。有一些选项可以在用户不注意的情况下静默更新令牌。该库在后台使用 iframe 来更新令牌。这是一个 link 的视频,其中库 Brock Allen 的作者解释了相同的概念。此功能的实现非常简单。如何使用该库的示例是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 中请求流的一部分,在服务器上实现类似的功能。请让我知道,如果你有任何问题。

谢谢, 索玛。

【讨论】:

    猜你喜欢
    • 2014-03-09
    • 2019-08-11
    • 2017-04-20
    • 1970-01-01
    • 2016-01-25
    • 2022-11-27
    • 2017-05-12
    • 2020-05-02
    • 2017-12-13
    相关资源
    最近更新 更多