Tenant-Routed Login
Use this pattern when:
- tenant users authenticate on tenant subdomains such as
acme.example.com - platform admins authenticate on a separate root or admin domain such as
app.example.com - different tenants need different login providers or different OIDC app registrations
This is supported by combining:
PrimusSaaS.Identity.BrokerPrimusSaaS.Identity.Broker.RoutingPrimusSaaS.MultiTenancy
The validated path in the current repo is:
- platform domains map to a platform auth policy
- tenant subdomains map to tenant auth policy from tenant metadata
- OIDC providers can change authority/client settings per tenant through
idp_config_referenceandIPrimusBrokerExternalProviderConfigurationResolver
Flow
app.example.com -> platform scope -> platform providers
acme.example.com -> tenant scope -> tenant metadata provider/config
globex.example.com -> tenant scope -> different tenant provider/config
Typical examples:
app.example.com->localor a central platform Entra ID app for support/admin usersacme.example.com-> Acme Entra IDglobex.example.com-> Globex Okta or Auth0
What To Configure
1. Multi-tenancy host model
builder.Services.AddPrimusMultiTenancy(options =>
{
options.EnableSubdomainResolution = true;
options.SubdomainBaseDomain = "example.com";
options.PlatformDomains = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"app.example.com",
"admin.example.com",
};
});
2. Broker + routing bridge
builder.Services.AddPrimusAuthBroker(builder.Configuration, builder.Environment.IsDevelopment());
builder.Services.AddPrimusBrokerTenantRoutingFromMultiTenancy(options =>
{
options.PlatformProviders = new() { "local", "azure" };
});
3. Tenant metadata
Each tenant record can declare its login policy in metadata:
var tenant = new TenantRecord(
TenantId: "tenant-acme",
DisplayName: "Acme",
Status: TenantStatus.Active,
Metadata: new Dictionary<string, string?>
{
["idp_provider"] = "azure",
["idp_config_reference"] = "azure:acme",
["idp_additional_providers"] = "local"
})
{
Slug = "acme"
};
Supported metadata keys in the routing bridge:
| Key | Meaning |
|---|---|
idp_provider | Primary provider key for the tenant, such as azure, google, auth0, okta, or local |
idp_config_reference | Per-tenant runtime config reference passed into the broker challenge flow |
idp_additional_providers | Optional comma-separated fallback providers |
idp_challenge_path | Optional explicit challenge path override |
4. Per-tenant OIDC runtime config
If tenant A and tenant B both use azure, but with different Entra tenants or app registrations, register a runtime config resolver:
builder.Services.AddSingleton<IPrimusBrokerExternalProviderConfigurationResolver>(
new TenantOidcConfigurationResolver());
public sealed class TenantOidcConfigurationResolver
: IPrimusBrokerExternalProviderConfigurationResolver
{
public ValueTask<PrimusBrokerExternalProviderConfiguration?> ResolveAsync(
HttpContext httpContext,
string provider,
string? configReference,
CancellationToken cancellationToken = default)
{
return configReference switch
{
"azure:acme" => ValueTask.FromResult<PrimusBrokerExternalProviderConfiguration?>(
new PrimusBrokerExternalProviderConfiguration(
Authority: "https://login.microsoftonline.com/acme-tenant-id/v2.0",
ClientId: "acme-client-id",
ClientSecret: "acme-client-secret")),
"azure:globex" => ValueTask.FromResult<PrimusBrokerExternalProviderConfiguration?>(
new PrimusBrokerExternalProviderConfiguration(
Authority: "https://login.microsoftonline.com/globex-tenant-id/v2.0",
ClientId: "globex-client-id",
ClientSecret: "globex-client-secret")),
_ => ValueTask.FromResult<PrimusBrokerExternalProviderConfiguration?>(null)
};
}
}
Login Entry Points
You have two usable entry patterns:
- Broker-native entry points:
GET /api/auth/providersand then provider-specific login URLs such as/api/auth/azure - Routing helper entry point:
app.MapPrimusBrokerTenantLoginEndpoint()and send users to/login
When routing middleware is active, the broker already filters GET /api/auth/providers by the resolved tenant auth policy. The /login helper is useful when you want one stable login route that redirects or returns local-login metadata automatically.
Platform Admin Separation
To keep platform admins on a separate IdP from tenant users:
- put admin users on a platform domain listed in
PlatformDomains - set
PlatformProvidersto the provider keys allowed for that domain - do not add those admin providers to tenant metadata unless you intentionally want them exposed to tenant routes
That gives you a clean split:
- platform domain -> admin providers
- tenant domain -> tenant provider policy
Limits
This package layer does not become your tenant registry or identity control plane.
- tenant records and onboarding still belong to your app
- downstream authorization still needs tenant/membership checks after sign-in
- the validated per-tenant runtime override path in the current repo is for OIDC provider configuration
Evidence In This Repo
The current source tree already contains evidence for this flow:
- tenant/platform route scope and auth policy tests in
PrimusSaaS.Identity.Broker.Tests - runtime external provider configuration override tests in
PrimusSaaS.Identity.Broker.Tests - a full integration write-up in MULTI_TENANT_AUTH_INTEGRATION_GUIDE.md