You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多B2C租户共享单一WebApp与API部署技术问询

Great question—this is a common scenario when building multi-tenant SaaS solutions with Azure AD B2C. Let's tackle each part separately since Web Apps and APIs handle dynamic tenant configurations differently.

Problem 1: Web App - Redirect to the Correct Tenant for Authentication

Instead of hardcoding tenant settings at startup, you can dynamically adjust the OpenIdConnectAuthenticationOptions per request using OpenID Connect events. The key is to hook into the OnRedirectToIdentityProvider event to fetch tenant-specific config (from your database/in-memory store) based on the incoming request URL, then update the authentication options on the fly.

Step-by-Step Implementation:

  1. Define a Tenant Configuration Provider
    Create a service to fetch tenant settings (tenant ID, client ID, sign-in policy, etc.) using the request's host/domain. This keeps your tenant logic decoupled from the auth pipeline.

    public interface ITenantConfigProvider
    {
        Task<TenantConfig> GetConfigByRequestUrl(HttpRequest request);
        Task<TenantConfig> GetConfigByClientId(string clientId); // For API later
    }
    
    public class TenantConfig
    {
        public string TenantId { get; set; }
        public string TenantName { get; set; } // e.g., "contoso" for contoso.onmicrosoft.com
        public string ClientId { get; set; }
        public string ClientSecret { get; set; }
        public string SignInPolicy { get; set; }
        public bool IsActive { get; set; }
    }
    
    // Example implementation using a database
    public class DatabaseTenantConfigProvider : ITenantConfigProvider
    {
        private readonly IDbConnection _dbConn;
    
        public DatabaseTenantConfigProvider(IDbConnection dbConn) => _dbConn = dbConn;
    
        public async Task<TenantConfig> GetConfigByRequestUrl(HttpRequest request)
        {
            var domain = request.Host.Host;
            return await _dbConn.QueryFirstOrDefaultAsync<TenantConfig>(
                "SELECT TenantId, TenantName, ClientId, ClientSecret, SignInPolicy, IsActive FROM Tenants WHERE Domain = @Domain",
                new { Domain = domain });
        }
    
        public async Task<TenantConfig> GetConfigByClientId(string clientId)
        {
            return await _dbConn.QueryFirstOrDefaultAsync<TenantConfig>(
                "SELECT TenantId, TenantName, ClientId, ClientSecret, SignInPolicy, IsActive FROM Tenants WHERE ClientId = @ClientId",
                new { ClientId = clientId });
        }
    }
    
  2. Configure OpenID Connect with Dynamic Settings
    In your Startup.cs, set up the auth pipeline with base settings, then use the OnRedirectToIdentityProvider event to inject tenant-specific config. You'll also need to handle the authorization code callback to ensure you use the correct tenant settings to exchange the code for tokens.

    services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
        .AddOpenIdConnect(options =>
        {
            // Base static config (no tenant-specific values)
            options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
            options.CallbackPath = "/signin-oidc";
            options.SaveTokens = true;
    
            options.Events = new OpenIdConnectEvents
            {
                OnRedirectToIdentityProvider = async context =>
                {
                    // Fetch tenant config based on the incoming request's domain
                    var tenantProvider = context.HttpContext.RequestServices.GetRequiredService<ITenantConfigProvider>();
                    var tenantConfig = await tenantProvider.GetConfigByRequestUrl(context.HttpContext.Request);
    
                    if (tenantConfig == null)
                    {
                        context.Response.StatusCode = StatusCodes.Status400BadRequest;
                        context.HandleResponse();
                        return;
                    }
    
                    // Update auth options with tenant-specific values
                    options.Authority = $"https://{tenantConfig.TenantName}.b2clogin.com/tfp/{tenantConfig.TenantId}.onmicrosoft.com/{tenantConfig.SignInPolicy}/v2.0/";
                    options.ClientId = tenantConfig.ClientId;
                    options.ClientSecret = tenantConfig.ClientSecret;
    
                    // Ensure the redirect URI matches the current request's scheme/domain
                    context.ProtocolMessage.RedirectUri = $"{context.HttpContext.Request.Scheme}://{context.HttpContext.Request.Host}{options.CallbackPath}";
                },
                OnAuthorizationCodeReceived = async context =>
                {
                    // Re-fetch tenant config to use the correct token endpoint
                    var tenantProvider = context.HttpContext.RequestServices.GetRequiredService<ITenantConfigProvider>();
                    var tenantConfig = await tenantProvider.GetConfigByRequestUrl(context.HttpContext.Request);
    
                    context.TokenEndpointRequest.Address = $"https://{tenantConfig.TenantName}.b2clogin.com/{tenantConfig.TenantId}.onmicrosoft.com/{tenantConfig.SignInPolicy}/oauth2/v2.0/token";
                }
            };
        });
    
    // Register your tenant provider with DI
    services.AddScoped<ITenantConfigProvider, DatabaseTenantConfigProvider>();
    
  3. Cache Tenant Config
    Add caching (e.g., IDistributedCache) to your ITenantConfigProvider to avoid repeated database calls—this will significantly improve performance for high-traffic apps.

Problem 2: API - Validate Tokens from the Correct Tenant

For APIs, you need to dynamically validate tokens against the tenant that issued them. The core challenge is verifying the token's signature and issuer without hardcoding tenant-specific authorities. We'll use a custom IssuerValidator and fetch the tenant's public keys (JWKS) on-demand.

Step-by-Step Implementation:

  1. Configure JWT Bearer Authentication with Dynamic Validation
    In your API's Startup.cs, set up JWT bearer auth and override the IssuerValidator to dynamically fetch tenant config based on the token's audience (client ID). You'll also fetch the tenant's OpenID config to get valid signing keys for token verification.

    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.SaveToken = true;
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateAudience = true,
                ValidateIssuer = true,
                ValidateIssuerSigningKey = true,
                // Disable static issuer/key validation—we'll handle it dynamically
                ValidIssuers = null,
                IssuerSigningKeys = null,
    
                IssuerValidator = async (issuerToken, securityToken, validationParameters) =>
                {
                    var jwtToken = securityToken as JwtSecurityToken;
                    if (jwtToken == null)
                        throw new SecurityTokenInvalidIssuerException("Invalid token format");
    
                    // Get client ID from the token's audience claim
                    var clientId = jwtToken.Claims.FirstOrDefault(c => c.Type == "aud")?.Value;
                    if (string.IsNullOrEmpty(clientId))
                        throw new SecurityTokenInvalidIssuerException("Missing client ID in token");
    
                    // Fetch tenant config using the client ID
                    var tenantProvider = validationParameters.RequestServices.GetRequiredService<ITenantConfigProvider>();
                    var tenantConfig = await tenantProvider.GetConfigByClientId(clientId);
    
                    if (tenantConfig == null || !tenantConfig.IsActive)
                        throw new SecurityTokenInvalidIssuerException($"Invalid or inactive tenant for client ID {clientId}");
    
                    // Verify the issuer matches the expected format for the tenant
                    var expectedIssuer = $"https://{tenantConfig.TenantName}.b2clogin.com/{tenantConfig.TenantId}/v2.0/";
                    if (!string.Equals(issuerToken, expectedIssuer, StringComparison.OrdinalIgnoreCase))
                        throw new SecurityTokenInvalidIssuerException($"Invalid issuer: {issuerToken}. Expected: {expectedIssuer}");
    
                    // Fetch the tenant's OpenID config to get valid signing keys
                    var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(
                        $"https://{tenantConfig.TenantName}.b2clogin.com/{tenantConfig.TenantId}.onmicrosoft.com/{tenantConfig.SignInPolicy}/v2.0/.well-known/openid-configuration",
                        new OpenIdConnectConfigurationRetriever());
                    var openIdConfig = await configManager.GetConfigurationAsync(CancellationToken.None);
    
                    // Update validation parameters with the tenant's signing keys
                    validationParameters.IssuerSigningKeys = openIdConfig.SigningKeys;
    
                    return issuerToken;
                }
            };
    
            // Optional: Add post-validation checks (e.g., tenant status)
            options.Events = new JwtBearerEvents
            {
                OnTokenValidated = async context =>
                {
                    var jwtToken = context.SecurityToken as JwtSecurityToken;
                    var clientId = jwtToken.Claims.FirstOrDefault(c => c.Type == "aud")?.Value;
                    var tenantProvider = context.HttpContext.RequestServices.GetRequiredService<ITenantConfigProvider>();
                    var tenantConfig = await tenantProvider.GetConfigByClientId(clientId);
    
                    if (!tenantConfig.IsActive)
                    {
                        context.Fail("Tenant is inactive");
                    }
                }
            };
        });
    
    // Register the same tenant config provider as the web app
    services.AddScoped<ITenantConfigProvider, DatabaseTenantConfigProvider>();
    
  2. Cache OpenID Configs
    Cache the OpenIdConnectConfiguration for each tenant to avoid repeated calls to Azure AD B2C's well-known endpoint. Use IDistributedCache with a TTL (e.g., 24 hours) since signing keys don't change frequently.

Key Notes for Both Scenarios:

  • Security: Encrypt sensitive tenant data (like ClientSecret) in your database—never store plaintext secrets.
  • Callback URIs: Ensure each tenant's AAD B2C application registration includes the correct redirect URIs for your web app's domain(s).
  • Error Handling: Return clear, consistent error responses (e.g., 400 for invalid tenants, 401 for invalid tokens) to help clients debug issues.

内容的提问来源于stack exchange,提问作者Indi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:52:39