Skip to main content
Version: Current

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.Broker
  • PrimusSaaS.Identity.Broker.Routing
  • PrimusSaaS.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_reference and IPrimusBrokerExternalProviderConfigurationResolver

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 -> local or a central platform Entra ID app for support/admin users
  • acme.example.com -> Acme Entra ID
  • globex.example.com -> Globex Okta or Auth0

What To Configure

1. Multi-tenancy host model

Program.cs
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

Program.cs
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:

KeyMeaning
idp_providerPrimary provider key for the tenant, such as azure, google, auth0, okta, or local
idp_config_referencePer-tenant runtime config reference passed into the broker challenge flow
idp_additional_providersOptional comma-separated fallback providers
idp_challenge_pathOptional 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:

Program.cs
builder.Services.AddSingleton<IPrimusBrokerExternalProviderConfigurationResolver>(
new TenantOidcConfigurationResolver());
TenantOidcConfigurationResolver.cs
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/providers and 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 PlatformProviders to 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