背景故事:我正在尝试将使用 .NET 5 (MVC) 的新客户端应用程序与现有的 IdentityServer4 链接起来。IdentityServer4(简称 IS4)既用于对客户端进行身份验证,也用于提供声明和角色以及 API(单独的 webapp)所依赖的 access_token 以在后端系统上请求数据。在新客户端上,我使用 IdentityModel 包来处理身份验证和授权。到目前为止,我已经管理了身份验证和授权工作,但我遇到了有关 access_token 到期日期的问题。
我目前已将 IS4 中的客户端配置为具有以下功能。如果该行显示“(默认)”,则表示它是 IdentityModel/IS4 建议或设置的默认值:
- IdentityTokenLifetime:300s / 5m(默认)
- IdentityAccessToken:300s / 5m(缩短以允许我测试)
- AuthorizationCodeLifetime:300s / 5m(默认)
流程 第一个例子:
- 用户浏览网页,并被重定向到 IS4 登录。
- 用户填写 user/pass 并成功验证,并被重定向回 Web 应用程序的安全部分。
- 当用户点击安全网页时,会使用用户的 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();