多租户Blazor应用中Sustainsys.Saml2.AspNetCore2多IdP配置示例请求及运行时添加IdP可行性咨询
Great question! Setting up multiple Identity Providers (IdPs) with Sustainsys.Saml2.AspNetCore2 for your Blazor multi-tenant app is totally feasible, and runtime addition is also possible with a bit of extra work. Let's walk through both scenarios step by step.
1. Configuring Multiple IdPs at Startup
The core idea here is to register separate SAML2 authentication schemes for each IdP. Each scheme will have its own configuration tied to a specific IdP. Here's how to implement this in a .NET 6+ Blazor app (using top-level statements in Program.cs):
Step 1: Register Authentication Schemes for Each IdP
using Microsoft.AspNetCore.Authentication.Cookies; using Sustainsys.Saml2.AspNetCore2; using Sustainsys.Saml2.Metadata; var builder = WebApplication.CreateBuilder(args); // Add Blazor services builder.Services.AddRazorPages(); builder.Services.AddServerSideBlazor(); // Configure authentication builder.Services.AddAuthentication(options => { // Use cookie auth to persist sessions after SAML authentication options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; }) .AddCookie() // Register StubIdP as a dedicated SAML2 scheme .AddSaml2("Saml2-StubIdP", options => { // Set your Service Provider (SP) entity ID (matches your app's SAML endpoint) options.SPOptions.EntityId = new EntityId("https://your-blazor-app.com/Saml2"); // Add StubIdP metadata options.IdentityProviders.Add(new IdentityProvider( new EntityId("https://stubidp.sustainsys.com/Metadata"), options.SPOptions) { LoadMetadata = true, // Automatically load metadata from the IdP // Optional: Customize assertion consumer service path if needed // AssertionConsumerServices = new Dictionary<string, string> // { // { "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", "/saml2/Acs/StubIdP" } // } }); }) // Register Okta as another dedicated SAML2 scheme .AddSaml2("Saml2-Okta", options => { options.SPOptions.EntityId = new EntityId("https://your-blazor-app.com/Saml2"); // Add Okta metadata URL (replace with your Okta app's metadata URL) options.IdentityProviders.Add(new IdentityProvider( new EntityId("https://your-okta-domain.okta.com/app/your-app-id/sso/saml/metadata"), options.SPOptions) { LoadMetadata = true, // Optional: Enforce NameID format (e.g., email) // NameIdPolicy = new NameIdPolicy { Format = NameIdFormat.EmailAddress } }); }); var app = builder.Build(); // Configure middleware pipeline if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); // Enable authentication & authorization app.UseAuthentication(); app.UseAuthorization(); app.MapBlazorHub(); app.MapFallbackToPage("/_Host"); // Add a login endpoint to trigger SAML challenges per scheme app.MapGet("/login", (string scheme) => { return Results.Challenge( new AuthenticationProperties { RedirectUri = "/" }, new[] { scheme }); }); app.Run();
Step 2: Add Login Buttons in Your Blazor UI
Let users choose which IdP to authenticate with by adding buttons that trigger the corresponding scheme:
<div class="login-buttons"> <button class="btn btn-primary" @onclick="LoginWithStubIdP">Login with StubIdP</button> <button class="btn btn-secondary" @onclick="LoginWithOkta">Login with Okta</button> </div> @code { [Inject] private NavigationManager NavigationManager { get; set; } private void LoginWithStubIdP() { // Redirect to the login endpoint with the StubIdP scheme NavigationManager.NavigateTo("/login?scheme=Saml2-StubIdP"); } private void LoginWithOkta() { // Redirect to the login endpoint with the Okta scheme NavigationManager.NavigateTo("/login?scheme=Saml2-Okta"); } }
2. Adding IdPs at Runtime
To add IdPs without restarting your app, you'll need to dynamically register authentication schemes and their corresponding SAML2 options. Here's a practical implementation:
Step 1: Create a Dynamic IdP Manager Service
This service handles registering new schemes and options at runtime:
using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Options; using Sustainsys.Saml2.AspNetCore2; using Sustainsys.Saml2.Metadata; public class DynamicIdpManager { private readonly IAuthenticationSchemeProvider _schemeProvider; private readonly IOptionsMonitorCache<Saml2Options> _optionsCache; private readonly IDataProtectionProvider _dataProtectionProvider; public DynamicIdpManager( IAuthenticationSchemeProvider schemeProvider, IOptionsMonitorCache<Saml2Options> optionsCache, IDataProtectionProvider dataProtectionProvider) { _schemeProvider = schemeProvider; _optionsCache = optionsCache; _dataProtectionProvider = dataProtectionProvider; } public async Task AddIdpAsync(string schemeName, string idpMetadataUrl, string spEntityId) { // Check if the scheme already exists to avoid duplicates var existingScheme = await _schemeProvider.GetSchemeAsync(schemeName); if (existingScheme != null) { throw new InvalidOperationException($"Scheme '{schemeName}' is already registered."); } // Create and configure SAML2 options for the new IdP var samlOptions = new Saml2Options(_dataProtectionProvider.CreateProtector("Sustainsys.Saml2")); samlOptions.SPOptions.EntityId = new EntityId(spEntityId); samlOptions.IdentityProviders.Add(new IdentityProvider( new EntityId(idpMetadataUrl), samlOptions.SPOptions) { LoadMetadata = true }); // Cache the options for the new scheme _optionsCache.TryAdd(schemeName, samlOptions); // Register the new authentication scheme await _schemeProvider.AddSchemeAsync(new AuthenticationScheme( schemeName, $"SAML2 - {schemeName}", typeof(Saml2Handler))); } }
Step 2: Register the Manager & Add a Runtime IdP Endpoint
Add these lines to Program.cs to register the service and create an API endpoint for adding IdPs:
// Register the dynamic IdP manager builder.Services.AddScoped<DynamicIdpManager>(); // ... (existing code) // Add an API endpoint to add IdPs at runtime app.MapPost("/api/idp/add", async ( [FromBody] IdpConfig config, [FromServices] DynamicIdpManager idpManager) => { try { await idpManager.AddIdpAsync( config.SchemeName, config.MetadataUrl, config.SpEntityId); return Results.Ok("IdP registered successfully."); } catch (Exception ex) { return Results.BadRequest($"Failed to register IdP: {ex.Message}"); } }); // Helper class for the API request body public class IdpConfig { public string SchemeName { get; set; } = string.Empty; public string MetadataUrl { get; set; } = string.Empty; public string SpEntityId { get; set; } = string.Empty; }
Step 3: Persist & Load Dynamic IdPs on Startup
Dynamic schemes are lost when the app restarts, so you'll need to store IdP configurations in a database or other persistent store. Add logic to load them on app startup:
// In Program.cs, after building the app but before running it var app = builder.Build(); // Assume you have a service to fetch saved IdP configs var idpStore = app.Services.GetRequiredService<IIdpStore>(); var dynamicIdpManager = app.Services.GetRequiredService<DynamicIdpManager>(); var savedIdps = await idpStore.GetAllIdpsAsync(); foreach (var idp in savedIdps) { await dynamicIdpManager.AddIdpAsync(idp.SchemeName, idp.MetadataUrl, idp.SpEntityId); } // ... (rest of the app configuration)
Key Notes
- Each IdP uses a unique scheme name to avoid conflicts.
- For multi-tenant scenarios, you can tie schemes to specific tenants (e.g.,
Saml2-TenantA-Okta). - When adding IdPs at runtime, ensure you validate metadata URLs to prevent malicious configurations.
内容的提问来源于stack exchange,提问作者mjorrens

