ASP.NET Core中的AddOpenIdConnect和Refresh Tokens

21

我已将AddOpenIdConnect添加到我的ASP.NET Core 3.1 Razor应用程序的ConfigureServices方法中。 它很好用,但是当令牌过期时,我会从我的IDP收到401响应。

我看到一个示例展示了一种手动连接刷新令牌的方式。

但我不确定是否这样做。 微软的人似乎不可能没有考虑过刷新令牌。

ASP.NET Core 3.1有自动更新访问令牌的刷新令牌的方法吗?


我提出了一个类似的问题,可能会引起兴趣:https://github.com/AzureAD/microsoft-identity-web/issues/1256 - Mark Hobson
4个回答

34

以下是我的成果。由于我找不到有关在ASP.NET Core中如何使用Cookie进行刷新令牌的很多示例,因此我想在这里发布此内容。(我在问题中链接的那个示例存在问题。)

这只是我尝试使其工作的一部分。它还没有在任何生产环境中使用过。此代码放置在ConfigureServices方法中。

services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
    options.Events = new CookieAuthenticationEvents
    {
        // After the auth cookie has been validated, this event is called.
        // In it we see if the access token is close to expiring.  If it is
        // then we use the refresh token to get a new access token and save them.
        // If the refresh token does not work for some reason then we redirect to 
        // the login screen.
        OnValidatePrincipal = async cookieCtx =>
        {
            var now = DateTimeOffset.UtcNow;
            var expiresAt = cookieCtx.Properties.GetTokenValue("expires_at");
            var accessTokenExpiration = DateTimeOffset.Parse(expiresAt);
            var timeRemaining = accessTokenExpiration.Subtract(now);
            // TODO: Get this from configuration with a fall back value.
            var refreshThresholdMinutes = 5;
            var refreshThreshold = TimeSpan.FromMinutes(refreshThresholdMinutes);

            if (timeRemaining < refreshThreshold)
            {
                var refreshToken = cookieCtx.Properties.GetTokenValue("refresh_token");
                // TODO: Get this HttpClient from a factory
                var response = await new HttpClient().RequestRefreshTokenAsync(new RefreshTokenRequest
                {
                    Address = tokenUrl,
                    ClientId = clientId,
                    ClientSecret = clientSecret,
                    RefreshToken = refreshToken
                });

                if (!response.IsError)
                {
                    var expiresInSeconds = response.ExpiresIn;
                    var updatedExpiresAt = DateTimeOffset.UtcNow.AddSeconds(expiresInSeconds);
                    cookieCtx.Properties.UpdateTokenValue("expires_at", updatedExpiresAt.ToString());
                    cookieCtx.Properties.UpdateTokenValue("access_token", response.AccessToken);
                    cookieCtx.Properties.UpdateTokenValue("refresh_token", response.RefreshToken);
                    
                    // Indicate to the cookie middleware that the cookie should be remade (since we have updated it)
                    cookieCtx.ShouldRenew = true;
                }
                else
                {
                    cookieCtx.RejectPrincipal();
                    await cookieCtx.HttpContext.SignOutAsync();
                }
            }
        }
    };
})
.AddOpenIdConnect(options =>
{
    options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    
    options.Authority = oidcDiscoveryUrl;
    options.ClientId = clientId;
    options.ClientSecret = clientSecret;

    options.RequireHttpsMetadata = true;
    
    options.ResponseType = OidcConstants.ResponseTypes.Code;
    options.UsePkce = true;
    // This scope allows us to get roles in the service.
    options.Scope.Add("openid");
    options.Scope.Add("profile");
    options.Scope.Add("offline_access");

    // This aligns the life of the cookie with the life of the token.
    // Note this is not the actual expiration of the cookie as seen by the browser.
    // It is an internal value stored in "expires_at".
    options.UseTokenLifetime = false;
    options.SaveTokens = true;
});

这段代码分为两部分:

  1. AddOpenIdConnect:此部分代码设置应用程序的OIDC。关键设置如下:
    • SignInScheme:这让ASP.NET Core知道您想使用cookie来存储身份验证信息。
    • *UseTokenLifetime:据我所知,这将在cookie中设置一个内部的“expires_at”值,以成为访问令牌的生命周期。(不是实际的cookie过期时间,它保持在会话级别。)
    • *SaveTokens:据我所知,这是导致令牌保存在cookie中的原因。
  2. OnValidatePrincipal:当cookie已验证时,将调用此部分。在此部分中,我们检查访问令牌是否接近或过期。如果是,则刷新它,并将更新后的值存储在cookie中。如果无法刷新令牌,则将用户重定向到登录屏幕。

该代码使用必须来自您的配置文件的以下值:

  • clientId:OAuth2客户端ID。也称为客户端密钥、消费者密钥等。
  • clientSecret:OAuth2客户端密钥。也称为消费者密钥等。
  • oidcDiscoveryUrl:您的IDP的Well Known配置文档的基本部分URL。如果您的Well Known配置文档位于https://youridp.domain.com/oauth2/oidcdiscovery/.well-known/openid-configuration,则此值将为https://youridp.domain.com/oauth2/oidcdiscovery
  • tokenUrl:指向您的IDP令牌端点的URL。例如:https:/youridp.domain.com/oauth2/token
  • refreshThresholdMinutes:如果您等到访问令牌接近过期,那么您就有可能在刷新它之前失败依赖于访问令牌的调用。(如果距离过期还有5毫秒,则它可能会在您有机会刷新它之前过期并失败调用。)此设置是您希望考虑准备刷新访问令牌的分钟数。

* 我是ASP.NET Core的新手。因此,我不100%确定这些设置是否做我所想的事情。这只是一段对我有效的代码,我想分享它。它可能适用于您,也可能不适用。


1
@Ergec - 我刚刚将令牌生命周期更新为false。后来的使用表明,如果设置为true会导致问题。 - Vaccano
@Vaccano非常感谢。 - Ergec
@Vaccano 谢谢!我一直在这个项目上忙忙碌碌了一段时间,非常感谢! - Rodney Pannell

1
据我所知,ASP.NET Core 3.1 中没有内置自动刷新访问令牌的功能。但是,我发现了 IdentityServer4 作者提供的这个方便的,它将访问令牌和刷新令牌存储在内存中(可以覆盖),并在您从库中请求访问令牌时自动刷新访问令牌。
如何使用该库:https://identitymodel.readthedocs.io/en/latest/aspnetcore/web.html
NuGet 包:https://www.nuget.org/packages/IdentityModel.AspNetCore/
源代码:https://github.com/IdentityModel/IdentityModel.AspNetCore

从2022年开始,IdS默认启用了刷新令牌轮换。这意味着每次访问令牌请求时,刷新令牌会自动更新,因此您无需进行任何特殊操作,一切都已经设置好了。 - Niksr

1
我最近在一个.NET 7.0示例中实现了令牌刷新。许多 MS OIDC 堆栈,包括旧的 Owin、.NET Core 等,一直有刷新令牌和重写 cookie 的选项。然而,文档似乎记录不够清晰,我不得不在 aspnet 源代码中查找 cookie 重写步骤。因此,我想在这个帖子中补充一些内容,以便未来的读者可以受益。
刷新令牌授权
首先发送一个符合标准的“刷新令牌授权”请求:
private async Task<JsonNode> RefreshTokens(HttpContext context)
{
    var tokenEndpoint = "https://login.example.com/oauth/v2/token";
    var clientId = "myclientid";
    var clientSecret = "myclientsecret";
    var refreshToken = await context.GetTokenAsync("refresh_token");

    var requestData = new[]
    {
        new KeyValuePair<string, string>("client_id", clientId),
        new KeyValuePair<string, string>("client_secret", clientSecret),
        new KeyValuePair<string, string>("grant_type", "refresh_token"),
        new KeyValuePair<string, string>("refresh_token", refreshToken),
    };

    using (var client = new HttpClient())
    {
        client.DefaultRequestHeaders.Add("accept", "application/json");
        
        var response = await client.PostAsync(tokenEndpoint, new FormUrlEncodedContent(requestData));
        response.EnsureSuccessStatusCode();

        var json = await response.Content.ReadAsStringAsync();
        return JsonNode.Parse(json).AsObject();
    }
}

重写Cookies

接下来,通过使用一组新的令牌进行“登录”来重写Cookies。更好的方法名称可能是类似于“更新身份验证状态”的内容。如果您查看HTTP响应,则会看到一个更新的set-cookie标头,其中包含新的令牌。

请注意,在刷新令牌授权响应中,您可能会收到新的刷新令牌和新的ID令牌,也可能不会。如果没有,那么请提供现有值。

private async Task RewriteCookies(JsonNode tokens, HttpContext context)
{   
    var accessToken = tokens["access_token"]?.ToString();
    var refreshToken = tokens["refresh_token"]?.ToString();
    var idToken = tokens["id_token"]?.ToString();

    var newTokens = new List<AuthenticationToken>();
    newTokens.Add(new AuthenticationToken{ Name = "access_token", Value = accessToken });

    if (string.IsNullOrWhiteSpace(refreshToken))
    {
        refreshToken = await context.GetTokenAsync("refresh_token");
    }
    newTokens.Add(new AuthenticationToken{ Name = "refresh_token", Value = refreshToken });

    if (string.IsNullOrWhiteSpace(idToken))
    {
        idToken = await context.GetTokenAsync("id_token");
    }
    newTokens.Add(new AuthenticationToken{ Name = "id_token", Value = idToken });

    var properties = context.Features.Get<IAuthenticateResultFeature>().AuthenticateResult.Properties;
    properties.StoreTokens(newTokens);
    await context.SignInAsync(context.User, properties);
}

摘要

在任何Web应用程序中,当您从API收到401响应时能够刷新访问令牌是一项基本功能。使用短暂的访问令牌和类似上面的代码来更新它们,以获得良好的可用性。

请注意,仅依赖到期时间并不完全可靠。在某些情况下,由于基础设施事件,API令牌验证可能会失败。然后API会返回401以表示未过期的访问令牌。Web应用程序应通过刷新,然后重试API请求来处理此问题。


0

AddOpenIdConnect 用于配置执行 OpenID Connect 协议以从身份提供程序获取令牌的处理程序。但它不知道您想要保存令牌的位置。以下是可能的位置:

  • Cookie
  • 内存
  • 数据库

您可以将令牌存储在 cookie 中,然后检查令牌的过期时间并通过拦截 cookie 验证事件来刷新令牌(如示例所示)。

但是,AddOpenIdConnect 没有控制用户要将令牌存储在哪里和自动实现令牌刷新的逻辑。

您也可以尝试将中间件作为 ADAL.NET/MSAL.NET 包装,以提供缓存功能,然后您可以静默地获取/刷新令牌。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接