刷新IdentityServer4客户端中的访问令牌

时间:2016-12-15 15:57:22

标签: asp.net-mvc openid-connect identityserver4

我想知道如何使用混合流刷新IdentityServer4客户端中的访问令牌,并使用ASP.NET Core MVC构建。

如果我正确理解了整个概念,客户首先需要拥有" offline_access"范围,以便能够使用刷新令牌,这是启用短期访问令牌的最佳做法,以及撤销刷新令牌的能力,以防止向客户端发出任何新的访问令牌。

我成功获得了访问令牌和刷新令牌,但是如何处理MVC客户端中访问令牌的实际更新过程?

OpenId Connect(OIDC)中间件可以自动处理吗?或者我应该通过基本检查访问令牌是否已过期或将很快过期(即将到来的30秒)来检查访问令牌的到期时间,然后通过调用令牌端点来刷新访问令牌。使用刷新令牌?

是否建议在我的Controller操作方法中使用IdentityModel2TokenClient扩展方法RequestRefreshTokenAsync来调用令牌端点?

我已经看到OIDC中间件事件中的代码请求访问令牌并使用响应存储包含过期日期时间的声明。问题是我的OIDC已经以某种方式自动请求访问令牌,因此在收到第一个访问令牌之后直接请求新访问令牌并不是一件好事。

Controller操作方法示例没有访问令牌刷新逻辑:

public async Task<IActionResult> GetInvoices()
    {
        var token = await HttpContext.Authentication.GetTokenAsync("access_token");

        var client = new HttpClient();
        client.SetBearerToken(token);

        var response = await client.GetStringAsync("http://localhost:5001/api/getInvoices");
        ViewBag.Json = JArray.Parse(response).ToString();

        return View();
    }

3 个答案:

答案 0 :(得分:14)

OIDC中间件将为您处理此问题。它在检测到HTTP 401响应时执行,然后将用户重定向到IdentityServer登录页面。重定向到MVC应用程序后,它会将声明转换为ClaimsIdentity并将其传递给Cookie中间件,这将实现会话cookie。

只要cookie仍然有效,其他所有请求都不会涉及OIDC中间件。

所以你必须自己照顾好这个。您要考虑的另一件事是,无论何时您要刷新访问令牌,您都必须更新现有令牌,这样您就不会丢失它。如果您不这样做,会话cookie将始终包含相同的令牌 - 原始令牌 - 并且您每次都会刷新它。

我找到的解决方案是将其挂钩到Cookies中间件。 这是一般流程:

  • 在每个请求中,使用Cookie中间件事件来检查访问令牌
  • 如果它接近到期时间,请申请新的
  • ClaimsIdentity
  • 中替换新访问权限并刷新令牌
  • 指示Cookie中间件续订会话cookie,使其包含新令牌

我喜欢这种方法的是,在您的MVC代码中,您几乎可以保证始终拥有有效的访问令牌,除非引用该令牌连续多次失败。

我不喜欢的是它与MVC非常相关 - 更具体地说是Cookie中间件 - 所以它不是真正的便携式。

你可以看看我放在一起的this GitHub repo。它确实使用IdentityModel,因为它可以处理所有事情并隐藏您必须对IdentityServer进行的HTTP调用的大部分复杂性。

答案 1 :(得分:1)

我创建了一个基于动作过滤器的解决方案,以便在ASP.NET Core 2.0中使用OIDC中间件。

AJAX请求也将通过动作过滤器进行更新访问令牌/刷新令牌。

https://gist.github.com/devJ0n/43c6888161169e09fec542d2dc12af09

答案 2 :(得分:0)

我发现了两种可能的解决方案,两者都是相同的,但在OIDC中间件的不同时间发生。在事件中,我提取访问令牌过期时间值并将其存储为声明,稍后可用于检查是否可以使用当前访问令牌调用We​​b API,或者我是否应该使用刷新请求新的访问令牌令牌。

如果有人可以提供关于哪些事件更适合使用的任何意见,我将不胜感激。

var oidcOptions = new OpenIdConnectOptions
{
      AuthenticationScheme = appSettings.OpenIdConnect.AuthenticationScheme,
      SignInScheme = appSettings.OpenIdConnect.SignInScheme,

      Authority = appSettings.OpenIdConnect.Authority,
      RequireHttpsMetadata = _hostingEnvironment.IsDevelopment() ? false : true,
      PostLogoutRedirectUri = appSettings.OpenIdConnect.PostLogoutRedirectUri,

      ClientId = appSettings.OpenIdConnect.ClientId,
      ClientSecret = appSettings.OpenIdConnect.ClientSecret,
      ResponseType = appSettings.OpenIdConnect.ResponseType,

      UseTokenLifetime = appSettings.OpenIdConnect.UseTokenLifetime,
      SaveTokens = appSettings.OpenIdConnect.SaveTokens,
      GetClaimsFromUserInfoEndpoint = appSettings.OpenIdConnect.GetClaimsFromUserInfoEndpoint,

      Events = new OpenIdConnectEvents
      {
          OnTicketReceived = TicketReceived,
          OnUserInformationReceived = UserInformationReceived
      },

      TokenValidationParameters = new TokenValidationParameters
      {                    
          NameClaimType = appSettings.OpenIdConnect.NameClaimType,
          RoleClaimType = appSettings.OpenIdConnect.RoleClaimType
      }
  };
  oidcOptions.Scope.Clear();
  foreach (var scope in appSettings.OpenIdConnect.Scopes)
  {
      oidcOptions.Scope.Add(scope);
  }
  app.UseOpenIdConnectAuthentication(oidcOptions);

以下是我可以选择的一些事件示例:

        public async Task TicketReceived(TicketReceivedContext trc)
    {
        await Task.Run(() =>
        {
            Debug.WriteLine("TicketReceived");

            //Alternatives to get the expires_at value
            //var expiresAt1 = trc.Ticket.Properties.GetTokens().SingleOrDefault(t => t.Name == "expires_at").Value;
            //var expiresAt2 = trc.Ticket.Properties.GetTokenValue("expires_at");
            //var expiresAt3 = trc.Ticket.Properties.Items[".Token.expires_at"];

            //Outputs:
            //expiresAt1 = "2016-12-19T11:58:24.0006542+00:00"
            //expiresAt2 = "2016-12-19T11:58:24.0006542+00:00"
            //expiresAt3 = "2016-12-19T11:58:24.0006542+00:00"

            //Remove OIDC protocol claims ("iss","aud","exp","iat","auth_time","nonce","acr","amr","azp","nbf","c_hash","sid","idp")
            ClaimsPrincipal p = TransformClaims(trc.Ticket.Principal);

            //var identity = p.Identity as ClaimsIdentity;

            // keep track of access token expiration
            //identity.AddClaim(new Claim("expires_at1", expiresAt1.ToString()));
            //identity.AddClaim(new Claim("expires_at2", expiresAt2.ToString()));
            //identity.AddClaim(new Claim("expires_at3", expiresAt3.ToString()));

            //Todo: Check if it's OK to replace principal instead of the ticket, currently I can't make it work when replacing the whole ticket.
            //trc.Ticket = new AuthenticationTicket(p, trc.Ticket.Properties, trc.Ticket.AuthenticationScheme);
            trc.Principal = p;                
        });
    }

我也有UserInformationReceived事件,我不确定是否应该使用它而不是TicketReceived事件。

        public async Task UserInformationReceived(UserInformationReceivedContext uirc)
    {
        await Task.Run(() =>
        {
            Debug.WriteLine("UserInformationReceived");

            ////Alternatives to get the expires_at value
            //var expiresAt4 = uirc.Ticket.Properties.GetTokens().SingleOrDefault(t => t.Name == "expires_at").Value;
            //var expiresAt5 = uirc.Ticket.Properties.GetTokenValue("expires_at");
            //var expiresAt6 = uirc.Ticket.Properties.Items[".Token.expires_at"];
            //var expiresIn1 = uirc.ProtocolMessage.ExpiresIn;

            //Outputs:
            //expiresAt4 = "2016-12-19T11:58:24.0006542+00:00"
            //expiresAt5 = "2016-12-19T11:58:24.0006542+00:00"
            //expiresAt6 = "2016-12-19T11:58:24.0006542+00:00"
            //expiresIn = "60" <-- The 60 seconds test interval for the access token lifetime is configured in the IdentityServer client configuration settings

            var identity = uirc.Ticket.Principal.Identity as ClaimsIdentity;

            //Keep track of access token expiration
            //Add a claim with information about when the access token is expired, it's possible that I instead should use expiresAt4, expiresAt5 or expiresAt6 
            //instead of manually calculating the expire time.
            //This claim will later be checked before calling Web API's and if needed a new access token will be requested via the IdentityModel2 library.
            //identity.AddClaim(new Claim("expires_at4", expiresAt4.ToString()));
            //identity.AddClaim(new Claim("expires_at5", expiresAt5.ToString()));
            //identity.AddClaim(new Claim("expires_at6", expiresAt6.ToString()));
            //identity.AddClaim(new Claim("expires_in1", expiresIn1.ToString()));
            identity.AddClaim(new Claim("expires_in", DateTime.Now.AddSeconds(Convert.ToDouble(uirc.ProtocolMessage.ExpiresIn)).ToLocalTime().ToString()));
            //identity.AddClaim(new Claim("expires_in3", DateTime.Now.AddSeconds(Convert.ToDouble(uirc.ProtocolMessage.ExpiresIn)).ToString()));

            //The following is not needed when to OIDC middleware CookieAuthenticationOptions.SaveTokens = true
            //identity.AddClaim(new Claim("access_token", uirc.ProtocolMessage.AccessToken));
            //identity.Claims.Append(new Claim("refresh_token", uirc.ProtocolMessage.RefreshToken));
            //identity.AddClaim(new Claim("id_token", uirc.ProtocolMessage.IdToken));                
        });
    }