IdentityServer4作为外部提供商,如何避免注销提示?



我正在使用两个身份提供程序,它们都使用 ASP.NET MVC Core 2.2中的IdentityServer4实现。其中一个被另一个用作外部提供程序。让我们称它们为"主要"和"外部"。主提供程序由 Web 应用程序直接引用。外部提供程序是主提供程序提供的可选登录方法。

Web 应用程序使用 oidc-client-js 库来实现身份验证。Web 应用程序中的注销操作调用UserManager.signoutRedirect。当使用主身份提供程序时,这工作正常(不显示注销确认提示(。但是,当使用外部提供程序时,系统会提示用户从外部提供程序注销。

注销时的请求顺序为:

  • GET http://{primary}/connect/endsession?id_token_hint=...&post_logout_redirect_uri=http://{webapp}
  • GET http://{primary}/account/Logout?logoutId=...
  • GET http://{external}/connect/endsession?state=...&post_logout_redirect_uri=http://{primary}/signout-callback-{idp}&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=5.3.0.0
  • GET http://{external}/Account/Logout?logoutId=...

上面的最后一个请求显示了来自外部提供商的注销确认屏幕。

主提供程序上的/Account/Logout 页面的代码与文档中的示例代码几乎相同:

[HttpGet]
public async Task<IActionResult> Logout(string logoutId)
{
var vm = await BuildLogoutViewModelAsync(logoutId);
if (!vm.ShowLogoutPrompt)
{
// If the request is authenticated don't show the prompt,
// just log the user out by calling the POST handler directly.
return Logout(vm);
}
return View(vm);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout(LogoutInputModel model)
{
var vm = await BuildLoggedOutViewModelAsync(model.LogoutId);
if (User?.Identity.IsAuthenticated)
{
// delete local authentication cookie
await _signInManager.SignOutAsync();
// raise the logout event
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
}
// check if we need to trigger sign-out at an upstream identity provider
if (vm.TriggerExternalSignout)
{
// build a return URL so the upstream provider will redirect back
// to us after the user has logged out. this allows us to then
// complete our single sign-out processing.
var url = Url.Action("Logout", new { logoutId = vm.LogoutId });
// this triggers a redirect to the external provider for sign-out
var ap = new AuthenticationProperties { RedirectUri = url };
return SignOut(ap, vm.ExternalAuthenticationScheme);
}
return View("LoggedOut", vm);
}

BuildLogoutViewModelAsync方法调用GetLogoutContextAsync来检查注销是否已通过身份验证,如下所示:

public async Task<LogoutViewModel> BuildLogoutViewModelAsync(string logoutId)
{
var vm = new LogoutViewModel
{
LogoutId = logoutId,
ShowLogoutPrompt = true
};
var context = await _interaction.GetLogoutContextAsync(logoutId);
if (context?.ShowSignoutPrompt == false)
{
// It's safe to automatically sign-out
vm.ShowLogoutPrompt = false;
}
return vm;
}

BuildLoggedOutViewModelAsync方法基本上只是检查外部标识提供者,并在使用时设置TriggerExternalSignout属性。

我不想把它变成一堵代码墙,但我将包括用于配置主身份服务器ConfigureServices代码,因为它可能是相关的:

var authenticationBuilder = services.AddAuthentication();
authenticationBuilder.AddOpenIdConnect(openIdConfig.Scheme, "external", ConfigureOptions);
void ConfigureOptions(OpenIdConnectOptions opts)
{
opts.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
opts.SignOutScheme = IdentityServerConstants.SignoutScheme;
opts.Authority = openIdConfig.ProviderAuthority;
opts.ClientId = openIdConfig.ClientId;
opts.ClientSecret = openIdConfig.ClientSecret;
opts.ResponseType = "code id_token";
opts.RequireHttpsMetadata = false;
opts.CallbackPath = $"/signin-{openIdConfig.Scheme}";
opts.SignedOutCallbackPath = $"/signout-callback-{openIdConfig.Scheme}";
opts.RemoteSignOutPath = $"/signout-{openIdConfig.Scheme}";
opts.Scope.Clear();
opts.Scope.Add("openid");
opts.Scope.Add("profile");
opts.Scope.Add("email");
opts.Scope.Add("phone");
opts.Scope.Add("roles");
opts.SaveTokens = true;
opts.GetClaimsFromUserInfoEndpoint = true;
var mapAdditionalClaims = new[] { JwtClaimTypes.Role, ... };
foreach (string additionalClaim in mapAdditionalClaims)
{
opts.ClaimActions.MapJsonKey(additionalClaim, additionalClaim);
}
opts.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = JwtClaimTypes.Name,
RoleClaimType = JwtClaimTypes.Role
};
}

我的理解是,传递给第一个/connect/endsession 端点的id_token_hint参数将"验证"注销请求,这允许我们根据GetLogoutContextAsync返回的ShowSignoutPrompt属性绕过提示。但是,当用户重定向到外部提供程序时,不会发生这种情况。对SignOut的调用会生成第二个/connect/endsession URL,其中包含state参数,但没有id_token_hint

外部提供程序中的注销代码与上面显示的代码基本相同。当它调用GetLogoutContextAsync时,该方法不会将请求视为经过身份验证的请求,因此ShowSignoutPrompt属性为 true。

知道如何对外部提供商的请求进行身份验证吗?

最后一个代码块,你讨厌,但幸运的是,它包含一个重要的行:

opts.SaveTokens = true;

这允许您稍后还原从外部提供程序获得的id_token
然后,您可以将其用作"二级提示"。

if (vm.TriggerExternalSignout)
{
var url = Url.Action("Logout", new { logoutId = vm.LogoutId });
var props = new AuthenticationProperties {RedirectUri = url};
props.SetParameter("id_token_hint", HttpContext.GetTokenAsync("id_token"));
return SignOut(props, vm.ExternalAuthenticationScheme);
}

我遇到了与 OP 完全相同的问题,并且能够通过明确声明将根据此 Github 问题将 ID 令牌添加到注销请求中来纠正它

https://github.com/IdentityServer/IdentityServer4/issues/3510

options.SaveTokens = true; // required for single sign out
options.Events = new OpenIdConnectEvents // required for single sign out
{
OnRedirectToIdentityProviderForSignOut = async (context) => context.ProtocolMessage.IdTokenHint = await context.HttpContext.GetTokenAsync("id_token")
};

我想出了一个解决方案,尽管它似乎与示例中所做的相矛盾。

该问题似乎是由两行代码引起的,这两行代码都来自我们用作 IDP 实现基础的 IdentityServer 示例。问题代码位于"主要"IDP 中。

第一行在"启动.cs"中ConfigureServices

var authenticationBuilder = services.AddAuthentication();
authenticationBuilder.AddOpenIdConnect(openIdConfig.Scheme, "external", ConfigureOptions);
void ConfigureOptions(OpenIdConnectOptions opts)
{
opts.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
opts.SignOutScheme = IdentityServerConstants.SignoutScheme; // this is a problem

第二位是在 ExternalController.cs 中,在Callback方法中。在这里,我们偏离了样本,使用IdentityServerConstants.ExternalCookieAuthenticationScheme而不是IdentityConstants.ExternalScheme

// Read external identity from the temporary cookie
var result = await this.HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
// ...
// delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(
IdentityServerConstants.ExternalCookieAuthenticationScheme); // this is a problem

注销时发生的情况是:由于SignOutScheme被覆盖,它正在寻找不存在的cookie。简单地删除它并不能修复它,因为对SignOutAsync的调用已经删除了包含身份代码验证方案所需信息的 cookie。由于它无法对方案进行身份验证,因此它不会在对"外部"IDP 的请求中包含id_token_hint

我已经能够通过删除覆盖 Startup.cs 中SignOutScheme的代码并将删除ExternalCookieAuthenticationSchemecookie 的代码移动到 AccountController 中的Logout端点来解决此问题.cs:

// check if we need to trigger sign-out at an upstream identity provider
if (vm.TriggerExternalSignout)
{
// delete temporary cookie used during external authentication
await this.HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
// build a return URL so the upstream provider will redirect back...

这样,"临时"外部 cookie 将保留到需要为止,但在用户注销时将被删除。

我不确定这是否是"正确"的解决方案,但它似乎在我测试过的所有情况下都能正常工作。我也不确定为什么我们偏离了 ExternalController .cs 中的示例,但我怀疑这是因为我们有两个独立的 IDP,而不是一个具有单个独立 IDP 的站点。此外,当我们使用混合流时,该示例似乎正在使用隐式流。

最新更新