【问题标题】:Sustainsys.SAML2 with a multitenant application具有多租户应用程序的 Sustainsys.SAML2
【发布时间】:2021-10-30 05:04:09
【问题描述】:

我正在开发一个 Blazor 应用程序,它是一个多租户应用程序。

我正在使用 Sustainsys.Saml2.AspNetCore2 包。

我有一个正在运行的测试 Blazor 应用程序,它可以针对测试 https://stubidp.sustainsys.com IdP 或测试 OKTA IdP 帐户进行授权。到目前为止一切顺利!

我想要一个能够同时使用两个 IdP 的代码示例。我不确定如何通过 services.AddAuthentication().AddSaml2(options => 或其他方式配置 2 个 IdP。我知道如何配置一个但不超过一个。

这将帮助我了解如何为我们的多租户应用程序设置不同的 IdP。

我的后续问题是,是否可以在运行时向我的应用程序添加 IdP,而不是在应用程序启动和运行 startup.cs 时添加 IdP。

谢谢!

【问题讨论】:

  • 您可以共享用于将 blazor 应用程序连接到 stubidp.sustainsys.com IdP 的代码的任何更改吗?
  • @drewid 刚刚发布了我的发现作为对这篇文章的回答。

标签: sustainsys-saml2


【解决方案1】:

经过大量挖掘,我找到了问题的答案。将来我会在 GitHub 上发布一个完整的 Blazor 和 WebForms 测试应用程序,但现在,这里有一些关键信息。

在创建 Blazor 应用程序时,请确保您允许使用个人用户帐户登录,以便为登录和外部登录 (Saml) 获取适当的脚手架。如果您当前的 Blazor 应用程序没有 Identity Scaffolding,请按照在线步骤将脚手架添加到您的应用程序中。

在 Blazor 中,如果您希望每个 IdP 具有单独的 SAML 模块,您可以多次调用 .AddSaml2 并为每个 .AddSaml2 设置一个 IdP (options.IdentityProviders.Add...)。此配置的关键是在构造函数中命名每个 Saml2 提供程序,例如 .AddSaml2("Client1", "Client1", options => 和 .AddSaml2("Client2", "Client2", options =>。当你这样做时,您将看到这些模块名称中的每一个都显示为右侧登录页面上的按钮。

如果您想在一个模块中使用多个 IdP,那么您可以使用如下配置调用 .AddSaml2 一次:

            // OKTA example that works
            //options.SPOptions.EntityId = new EntityId("https://localhost:44312/Okta"); // MSJ - Change to the current URL (proper port) -https://localhost:44312/Saml2
            options.SPOptions.EntityId = new EntityId("https://example.com/"); // Dummy entry
            // options.SPOptions.ModulePath = "/Okta"; // Don't need this, default is "Saml2"
            options.SPOptions.ReturnUrl = new Uri("/counter", UriKind.Relative); // Note used if RelayStateUsedAsReturnUrl set below is true
            options.Notifications.AcsCommandResultCreated = AcsCommandResultCreated; // Needed to intercept the command and add the LoginProvider for the tenant if IdP initiated login
            options.Notifications.GetIdentityProvider = GetIdentityProvider;
            options.Notifications.SelectIdentityProvider = SelectIdentityProvider;
            //options.Notifications.MetadataCreated = MetadataCreated;
            options.Notifications.AuthenticationRequestCreated = AuthenticationRequestCreated;
            options.Notifications.Unsafe.TokenValidationParametersCreated = TokenValidationParametersCreated;
            options.SPOptions.ServiceCertificates.Add(new X509Certificate2("Sustainsys.Saml2.Tests.pfx", "")); // Sustainsys.Saml2.Tests.pfx - no password on this pfx file
                                                                                                               // For now, turned off Assertion encryption


        options.IdentityProviders.Add(new IdentityProvider(new EntityId("http://www.okta.com/exk2edycw57Obmc5i5d7"), options.SPOptions)
        {
            MetadataLocation = "https://dev-60124262.okta.com/app/exk2edycw57Obmc5i5d7/sso/saml/metadata", // Need this since EntityID is different than metadata location
            LoadMetadata = true,
            AllowUnsolicitedAuthnResponse = true, // Need this for IdP initiated login

            // Need this as well for IdP initiated login to tell SAML2 to redirect to the ExternalLogin page in the Account folder
            RelayStateUsedAsReturnUrl = true, // In OKTA or others, ensure the relay state is set to /Identity/Account/ExternalLogin?returnUrl=%2F&handler=Callback to ensure proper routing
        });

        options.IdentityProviders.Add(new IdentityProvider(
            new EntityId("https://stubidp.sustainsys.com/Metadata"), options.SPOptions)
        {
            LoadMetadata = true,
            AllowUnsolicitedAuthnResponse = true,
            RelayStateUsedAsReturnUrl = true, // Setting this always gives an error that the ReturnURL is not relative
        }); 
            //idp.SigningKeys.AddConfiguredKey(new X509Certificate2("okta.cert")); // Not needed since the cert is in the meta data.

            saml2Options = options; // External reference for the options object

我在多租户环境中工作,所以我需要做更多的事情(处理通知)才能让事情正常工作。如果您只有一个 IdP,则不需要做这些额外的事情,因为它会起作用。请注意,我计划动态加载这些 IdP,以便它们不会像本示例中所述那样存在。这就是为什么我有“saml2Options”对象引用,以便以后添加 IdP。

多租户通知码:

    private void TokenValidationParametersCreated(TokenValidationParameters validationParameters, IdentityProvider idp, XmlElement xmlElement)
    {
        // ** Will need to replace the URL with the subdomain tenant code
        // How do we get access to the Navigation Manager
        validationParameters.ValidAudience = "https://localhost:44312/Saml2"; // Was /Okta
        // validationParameters.ValidateAudience = false; // Could use this but set the ValidAudience
    }

    private void AuthenticationRequestCreated(Saml2AuthenticationRequest request, IdentityProvider a, IDictionary<string, string> dict)
    {
        // This does not seem to matter
        // ** Will need to replace the URL with the subdomain tenant code
        request.Issuer = new EntityId($"https://localhost:44312/Saml2"); // Was /Okta
    }

    private void MetadataCreated(EntityDescriptor request, Saml2Urls urls)
    {
        // ** Will need to replace the URL with the subdomain tenant code
        // Not seeing this get called - do not worry about it
        request.EntityId = new EntityId($"https://localhost:44312/Saml2"); 
    }

    private IdentityProvider SelectIdentityProvider(EntityId entityID, IDictionary<string, string> arg2)
    {
        return saml2Options.IdentityProviders[entityID];
    }

    private IdentityProvider GetIdentityProvider(EntityId entityID, IDictionary<string, string> arg2, IOptions arg3)
    {

        return saml2Options.IdentityProviders[entityID];
    }


    public void AcsCommandResultCreated(CommandResult arg1, Saml2Response arg2)
    {
        if (arg1.RelayData == null)
        {
            // Need to do this for IdP initiated logins. RelayData is null so we need to add the LoginProvider
            var x = new Dictionary<string, string>();
            x.Add("LoginProvider", "Saml2"); // Will always be "Saml2" if we stick with one Saml endpoint with multiple IdentityProviders
            arg1.RelayData = x;
        }
    }

请注意,要使 IdP 启动的登录正常工作,我需要将其放入 IdP 的 RelayState 中: /Identity/Account/ExternalLogin?returnUrl=%2F&handler=Callback

最后一个魔法是将 ExternalLogin.cshtml.cs 中的 OnPost 方法更改为:

    public IActionResult OnPost(string provider, string returnUrl = null)
    {
        // This is used for SP initiated login. So for this case, we know the URL and thus the tenantCode
        // Hard coded here for testing but extract the tenant code from the URL here or from the external login button on the 
        // login page (pass it in)
        // We are assuming one scheme (SSO provider) per tenant
        string tenantCode = "Dev";
        // Request a redirect to the external login provider.
        var redirectUrl = Url.Page("./ExternalLogin", pageHandler: "Callback", values: new { returnUrl, tenantCode });
        var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);

        // This is what they were talking about - add idp and scheme and THEN you will get the proper entityID in SelectIdentityProvider
        properties.Items.Add("idp", "http://www.okta.com/exk2edycw57Obmc5i5d7"); // Put in the actual idp EntityID here - hardcoded here for testing
        properties.Items.Add("scheme", "Saml2");
        properties.Items.Add("tenant", "localhost"); // Used in notifications
        return new ChallengeResult(provider, properties);
    }

添加“idp”和“scheme”是允许通知报告多租户环境中使用的实际 IdP 的必要条件。同样,如果您只需要处理一个 IdP,则无需执行所有这些操作。

在 Blazor 中清理完所有内容后,我需要在 WebForms 中的旧版本应用程序中实现相同的内容。好玩!

【讨论】:

    【解决方案2】:

    选项的 IdentityProviders 属性是一个集合。只需向其中添加更多 IdentityProvider 对象即可。要选择要调用的 Idp,请使用键 idp 在 AuthProps 中放置一个项目,并将值设置为要使用的 Idp 的 EntityId。运行时可以更改集合。

    另外,更多的 Asp.Net-Core 方法是通过多次调用 AddSaml2 为每个 Idp 设置一个身份验证方案。这也可以在运行时完成,但更复杂。

    【讨论】:

    • 谢谢安德斯。我尝试添加多个 IdP,但只有添加的第一个出现在 ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); (登录.cshtml.cs 文件)。您能否发布有关添加多个 IdP 的代码 sn-p?多次调用 AddSaml2 的代码 sn-p 会很有帮助,而且我在执行此操作时会收到“System.InvalidOperationException:'Scheme already exists: Saml2'”的运行时错误。谢谢!
    猜你喜欢
    • 2014-02-10
    • 2016-06-08
    • 1970-01-01
    • 2013-03-24
    • 1970-01-01
    • 2013-06-15
    • 1970-01-01
    • 1970-01-01
    • 2018-04-05
    相关资源
    最近更新 更多