【问题标题】:IdentityServer4 as external provider, how to avoid logout prompt?IdentityServer4 作为外部提供者,如何避免注销提示?
【发布时间】:2020-08-07 14:46:45
【问题描述】:

我正在使用两个身份提供者,它们都使用 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 页面的代码与sample code in the documentation 几乎相同:

[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 的调用会生成第二个带有state 参数的/connect/endsession URL,但没有id_token_hint

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

知道如何验证对外部提供者的请求吗?

【问题讨论】:

    标签: c# asp.net-core-mvc identityserver4 openid-connect


    【解决方案1】:

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

    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);
    }
    

    【讨论】:

    • 我已经尝试过了,我能够检索令牌。不幸的是,SetParameter 调用不会导致id_token_hint 包含在对外部提供者的初始请求中。我也尝试使用StoreTokens 将其添加到身份验证属性中,但这也不起作用。
    • 当您为SignOut 调用传递一个全新的AuthenticationProperties 实例时,您不再需要存储任何内容,您只需将参数添加到您的注销请求和@987654330 @ 方法正是为了那个工作
    • 嗨! @JackA,这里有什么进展吗?在我的测试中,我使用了this 解决方案,因为它是上面列出的控制器中唯一的变化。您可以轻松尝试和比较。
    • 我花了一天左右的时间在 MS 源代码中寻找答案。不幸的是没有运气。由于它的优先级较低,我暂时搁置了这个问题,但我希望在某个时候再次处理它。
    • 我找到了解决方案(请参阅我的回复)。我很想听听你的想法。谢谢!
    【解决方案2】:

    我遇到了与 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")
      };
    

    【讨论】:

      【解决方案3】:

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

      问题似乎是由两行代码引起的,这两行代码都来自我们用作 IDP 实施基础的 IdentityServer 示例。问题代码在“主要”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 中,在 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 的代码并将删除 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 的站点。此外,当我们使用混合流时,示例似乎使用了隐式流。

      【讨论】:

      • 第二次阅读后,我明白了,您在“主要”IDP 中使用了 MS Identity,这改变了一切。从原始帖子中不可能意识到这一点。要制作有效的 IdSdv+Identity 对,有必要使用相应的 howto 作为起点。
      • @d_f 是的,我们正在 IDP 和 Web 应用程序(即 AngularJS)的后端服务中使用 MS Identity。 IDP 是在我开始这里的几年前创建的,所以我不确定到底是用什么作为起点。但是,我在其中看到的代码与文档中的代码示例大部分相同。您是否有指向您认为最合适的起点的链接?
      猜你喜欢
      • 2021-02-13
      • 2021-03-14
      • 2021-05-21
      • 2021-01-06
      • 2016-11-21
      • 2018-07-17
      • 2019-12-23
      • 2020-06-08
      • 1970-01-01
      相关资源
      最近更新 更多