C# IdentityServer4作为外部提供程序,如何避免注销提示?

C# IdentityServer4作为外部提供程序,如何避免注销提示?,c#,asp.net-core-mvc,identityserver4,openid-connect,C#,Asp.net Core Mvc,Identityserver4,Openid Connect,我正在使用两个身份提供者,它们都是在ASP.NET MVC Core 2.2中使用IdentityServer 4实现的。其中一个被另一个用作外部提供者。让我们称之为“主要”和“外部”。web应用程序直接引用主提供程序。外部提供程序是主提供程序提供的可选登录方法 web应用程序使用库来实现身份验证。web应用程序中的注销操作调用UserManager.signoutRedirect。当使用主标识提供程序时(不显示注销确认提示),此功能可以正常工作。但是,当使用外部提供程序时,会提示用户从外部提供

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

web应用程序使用库来实现身份验证。web应用程序中的注销操作调用
UserManager.signoutRedirect
。当使用主标识提供程序时(不显示注销确认提示),此功能可以正常工作。但是,当使用外部提供程序时,会提示用户从外部提供程序注销

注销时的请求顺序为:

  • 获取http://{primary}/connect/endsession?id_-token_-hint=…&post_-logout_-redirect_-uri=http://{webapp}
  • 获取http://{primary}/Account/Logout?logoutId=
  • 获取http://{external}/connect/endsession?state=…&post_logout_redirect_uri=http://{primary}/signout callback-{idp}&x-client-SKU=ID\u NETSTANDARD2\u 0&x-client-ver=5.3.0.0
  • 获取http://{external}/Account/Logout?logoutId=
上面的最后一个请求显示来自外部提供商的注销确认屏幕

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

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\u token\u hint
参数将“验证”注销请求,这允许我们根据
GetLogoutContextAsync
返回的
ShowSignoutPrompt
属性绕过提示。但是,当用户重定向到外部提供程序时,不会发生这种情况。调用
SignOut
生成第二个/connect/endsession URL,其中包含
state
参数,但没有
id\u token\u hint

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


您知道如何向外部提供者验证请求吗?

您不喜欢但幸运地添加的最后一段代码包含一行重要内容:

opts.SaveTokens=true;
这允许您稍后还原从外部提供程序获得的
id\u令牌。
然后您可以将其用作“第二级提示”

if(vm.TriggerExternalSignout)
{
var url=url.Action(“注销”,新的{logoutId=vm.logoutId});
var props=newauthenticationproperties{RedirectUri=url};
SetParameter(“id_token_hint”,HttpContext.GetTokenAsync(“id_token”);
返回注销(props、vm.ExternalAuthenticationScheme);
}
我提出了一个解决方案,尽管它似乎与示例中所做的相矛盾

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

第一行位于Startup.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中的
回调
方法。这里我们使用
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\u token\u hint

通过删除Startup.cs中覆盖
SignOutScheme
的代码,并将删除
ExternalCookieAuthenticationScheme
cookie的代码移动到AccountController.cs中的
Logout
端点,我已经解决了这个问题:

// 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。此外,当我们使用混合流时,该示例似乎使用隐式流。

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


我已经试过了,我能找回令牌。不幸的是,
SetParameter
调用没有导致
id\u token\u提示
包含在外部提供者的初始请求中。我还尝试使用
StoreTokens
将其添加到身份验证属性中,但这也不起作用。当您为
注销调用传递一个全新的
AuthenticationProperties
实例时,您不需要再存储任何内容,您只需将参数添加到您的注销请求中,
SetParameter
方法正是针对该作业的!杰卡,这里有进展吗?在我的tes中
// 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
// 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...
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")
  };