IdentityModel 实施:如何在到期日或之后验证(和刷新)access_token?

Posted

技术标签:

【中文标题】IdentityModel 实施:如何在到期日或之后验证(和刷新)access_token?【英文标题】:IdentityModel implementation: How to verify (and refresh) access_token on or after expiry date? 【发布时间】:2021-12-13 20:02:00 【问题描述】:

背景故事:我正在尝试将使用 .NET 5 (MVC) 的新客户端应用程序与现有 IdentityServer4 链接起来。 IdentityServer4(简称 IS4)既用于对客户端进行身份验证,也用于提供声明和角色以及 API(单独的 web 应用程序)依赖于在后端系统上请求数据的 access_token。在新客户端上,我使用 IdentityModel 包来处理身份验证和授权。到目前为止,我已经管理了身份验证和授权工作,但我遇到了关于 access_token 到期日期的问题。

我目前已将 IS4 中的客户端配置为具有以下功能。如果该行显示“(默认)”,则表示它是 IdentityModel/IS4 建议或设置的默认值:

IdentityTokenLifetime:300s / 5m(默认) IdentityAccessToken:300s / 5m(缩短以便我测试) AuthorizationCodeLifetime:300s / 5m(默认)

流程 第一个例子:

    用户浏览网页,被重定向到 IS4 登录。 用户填写用户名/密码并成功通过身份验证,并被重定向回网络应用的安全部分。 当用户点击安全网页时,会使用用户的 access_token 向外部 api 发出 api 请求以获取用户数据。 请求返回用户数据,网页返回该数据。 完美运行。

第二个例子:

    用户浏览网页,被重定向到 IS4 登录。 用户已使用 cookie 通过 IS4 进行身份验证,因此成功通过身份验证,并被重定向回 Web 应用的安全部分。 当用户点击安全网页时,会使用用户的 access_token 向外部 api 发出 api 请求以获取用户数据。 请求返回用户数据,网页返回该数据。 完美运行。

第三个例子:

    用户在网页上等待 15 分钟,然后刷新页面。 用户已登录网站,因此不会重定向到 IS4。 由于用户刷新,用户点击安全网页,使用用户的 access_token 向外部 api 发出 api 请求以获取用户数据。 请求返回空,因为 access_token 已过期(10 分钟前) 悲伤的笑脸:'(

第四个例子:

    以下示例三:用户看到错误并重新启动浏览器。 用户浏览网页,被重定向到 IS4 登录。 用户已使用 cookie 通过 IS4 进行身份验证,因此成功通过身份验证,并被重定向回 Web 应用的安全部分。 当用户点击安全网页时,会使用用户的 access_token 向外部 api 发出 api 请求以获取用户数据。 请求返回用户数据,网页返回该数据。 (因为 access_token 是新生成的,由于新的 IS4“登录”而具有未来的到期日期) 完美运行。

示例三是我遇到的问题。我期望发生的是 [Authorization]-check 不允许过期会话(access_tokens)通过,而是将用户重定向到 IS4 以根据用户仍然拥有的有效 cookie 自动重新验证(例如四)。

我试图解决的问题:

延长 IdentityAccessToken 生命周期:不能解决问题,而只是将问题移至新的 expire_date。 在我们现有的 IS4 实现中使用 IdentityModel 客户端“Web5”示例,显示相同的行为。

--

应用程序的要求是具有较短的 access_token 生命周期,以允许根据后端更改的声明/角色快速更新用户的权限和访问权限,同时允许“持久”登录以减少用户的时间”花费必须填写他们的帐户详细信息。

完全有可能不是技术问题,而是我的思维过程或对这些事情的理解是错误的。如果是这样,请告诉我流程应该是什么,最好是一个工作示例。

--

客户端中的IdentityModel配置如下:

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

services
    .AddAuthentication(options => 
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    )
    .AddCookie(options =>
    
        options.Events.OnSigningOut = async e =>
        
            // revoke refresh token on sign-out
            await e.HttpContext.RevokeUserRefreshTokenAsync();
        ;
    )
    .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => 
        options.GetClaimsFromUserInfoEndpoint = true;
        options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;

        options.Authority = Configuration.GetValue<string>("IdentityServer:Authority");
        options.ClientId = Configuration.GetValue<string>("IdentityServer:ClientId");
        options.ClientSecret = Configuration.GetValue<string>("IdentityServer:ClientSecret");
        options.RequireHttpsMetadata = Configuration.GetValue<bool>("IdentityServer:RequireHttpsMetadata");

        options.UsePkce = true;
        options.ResponseType = OidcConstants.ResponseTypes.CodeIdToken;
        options.SaveTokens = true;

        options.TokenValidationParameters = new TokenValidationParameters
        
            NameClaimType = JwtClaimTypes.Name,
            RoleClaimType = JwtClaimTypes.Role
        ;

        // Scopes
        options.Scope.Add("openid");
        options.Scope.Add("offline_access");
    )
    .AddOpenIdConnect("persistent", options => 
        options.CallbackPath = "/signin-persistent";
        options.Events = new OpenIdConnectEvents
        
            OnRedirectToIdentityProvider = context =>
            
                context.ProtocolMessage.Prompt = OidcConstants.PromptModes.None;
                return Task.FromResult<object>(null);
            ,

            OnMessageReceived = context => 
                if (string.Equals(context.ProtocolMessage.Error, "login_required", StringComparison.Ordinal))
                
                    context.HandleResponse();
                    context.Response.Redirect("/");
                
                return Task.FromResult<object>(null);
            
        ;
        
        ...
        // Rest of 'persistent' is similar as the non-persistent one
        ... 
    );
    
// Examples of IdentityModel suggest that calling this function make the boilerplate tasks of refreshing tokens and alike automatically work
services.AddAccessTokenManagement();

【问题讨论】:

【参考方案1】:

对于此流程,使用后端应用程序的解决方案是使用刷新令牌,可以通过请求 offline_access 范围并确保客户端配置为允许它们来获得。

刷新令牌与访问令牌一起返回,并且可用于在初始令牌过期后获取新的访问令牌(通过反向通道令牌端点调用)。这可以在第一次失败时完成(即来自 API 的 401 响应)或基于访问令牌的到期时间(通过使用 expires_in 令牌端点响应值或访问令牌本身中的 exp 声明.

查看:https://identityserver4.readthedocs.io/en/latest/topics/refresh_tokens.html

还有样本:https://github.com/IdentityServer/IdentityServer4/tree/main/samples/Clients/src/MvcAutomaticTokenManagement

【讨论】:

感谢您的链接和评论。在我的应用程序中,我已经在使用“offline_access”范围。至于例子,使用底部链接,唯一的区别是cookie过期时间;其余配置看起来相同。然而,我觉得我错过了一些东西。不就是“services.AddAccessTokenManagement();”的服务吗?到期时应该自动刷新令牌吗?如果是这样,根据我最初帖子中的配置,这可能是什么原因? (我将编辑我的初始帖子以添加我正在使用的范围) 好问题。当您需要令牌时,您实际上是如何检索令牌的? “HttpContext.GetTokenAsync("access_token")”或“HttpContext.GetUserAccessTokenAsync();”。在第一种情况下,令牌从初始 IS4 登录中填充,但在会话期间似乎保持不变。第二种方法只能刷新一次令牌并返回新令牌(如果需要),但也不更新 HttpContext 中的令牌,因此我只能在该函数调用期间使用该令牌。我希望为整个 HttpContext 更新令牌,这意味着可以在 [Authorize(Roles = "XX")] 检查中使用更新/更改的角色 如果是我,我希望设置一个场景来证明自动更新正在被触发,或者通过监视日志记录或实现一个钩子(覆盖的类、事件处理程序等)我可以设置断点或登录。您确定使用示例中的 HttpClient 管道吗?如果您深入研究代码,所有这些都挂在为 HttpClient 绑定注册的处理程序上:github.com/IdentityModel/IdentityModel.AspNetCore/blob/… 示例中使用的精确版本:github.com/IdentityModel/IdentityModel.AspNetCore/blob/… - 您将看到它在第 58 行的方法中注册客户端并附加处理程序

以上是关于IdentityModel 实施:如何在到期日或之后验证(和刷新)access_token?的主要内容,如果未能解决你的问题,请参考以下文章

idea评估许可证到期啥意思

P3909 异或之积

洛谷——P3909 异或之积

如何在 .NET 3.5 中使用 System.IdentityModel.Tokens.Jwt 5.2.1?

BZOJ3689异或之 堆+可持久化Trie树

我如何在 ASP.Net Core 2.1 mvc 应用程序中包含 System.Identitymodel 4.0