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

时间:2016-05-22 02:29:45

标签: oauth-2.0 access-token

方案

我正在使用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()     .Authentication     .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)不做任何事情并通过调用带有过期令牌的服务来捕获异常

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

这些解决方案都不是特别优雅。别人怎么处理这个?

1 个答案:

答案 0 :(得分:1)

更新

我花了一些时间查看有关如何在服务器端使用当前设置有效实现此功能的各种选项。

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

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

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

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

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

  5. 这是来自Stackoverflow的question,它有一些关于如何使用依赖注入实现AuthorizationFilter的很好的细节。

    将Authorization标头附加到服务:

    这在您的操作方法中发生。此时您确定令牌有效。所以我将创建一个抽象基类来实例化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解决方案对我来说更优雅。但是,我确信你的要求可能会迫使你按照自己的方式去做。如果您有任何问题,请告诉我。谢谢。

    添加访问令牌,刷新令牌并过期到声明并将其传递给以下服务可能不是一个好的解决方案。声明更适合于识别用户信息/授权信息。此外,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);
        });
    }
    

    mgr是

    的一个例子

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

    谢谢, 索玛。