Skip to content

Tenancy in AuthProxy

One deployment, many customers: acme.example.com and contoso.example.com hit the same proxy, and every downstream query must be scoped to the right tenant. Somebody has to answer “which tenant is this request for?” — once, reliably, at the edge. That’s AuthProxy’s tenancy system. The answer is forwarded to your services as the Tenant-ID header (see Identity for the full header set), so your application never re-derives it.

TenantResolutions is an ordered list of strategies — AuthProxy tries each in order until one resolves a tenant:

{
"Cratis": {
"AuthProxy": {
"TenantResolutions": [
{ "Strategy": "Host" },
{ "Strategy": "Claim", "Options": { "ClaimType": "tid" } }
]
}
}
}
StrategyResolves the tenant from…
Hostthe request host, matched against configured Domains / SourceIdentifiers
SubHosta subdomain convention (e.g. acme.example.comacme) — handy for SaaS provisioning
Claima claim on the authenticated user, matched against SourceIdentifiers
Routea regex over the request path, matched against SourceIdentifiers
Specifieda fixed tenant id (single-tenant deployments)
Defaulta fallback tenant id when nothing else matched
Selectiona .cratis-tenant cookie set by the built-in tenant-selection page

Each strategy takes its own Options. Claim takes a ClaimType (defaulting to the Microsoft tenant claim, http://schemas.microsoft.com/identity/claims/tenantid); Route takes a Pattern whose regex must expose a named group called sourceIdentifier; Specified and Default take a TenantId; SubHost takes the ParentHost to strip; Selection takes a TenantsEndpoint (below).

For Host, Claim, and Route, the strategy produces a source identifier which is then looked up in the Tenants registry — the tenant id is the key it maps to:

{
"Cratis": {
"AuthProxy": {
"TenantResolutions": [
{ "Strategy": "Host" }
],
"Tenants": {
"acme": { "Domains": ["acme.example.com"], "SourceIdentifiers": ["acme"] },
"contoso": { "Domains": ["contoso.example.com"], "SourceIdentifiers": ["contoso"] }
}
}
}
}

SubHost is the deliberate exception: the subdomain becomes the tenant id directly, with no registry lookup. That’s what makes it fit dynamically-provisioned SaaS — a new customer’s subdomain works without a configuration change. Because nothing proves such a tenant exists, pair SubHost with verification:

{
"Strategy": "SubHost",
"Options": {
"ParentHost": "example.com",
"VerificationUrlTemplate": "https://platform.example.com/api/tenants/{tenantId}"
}
}

Only single-segment subdomains resolve (acme.example.comacme; foo.bar.example.com does not), and sign-in flows that bounce through a shared auth host redirect back to the tenant host afterwards — see the tenant-aware sign-in note on the Authentication page.

Letting the user pick: the Selection strategy

Section titled “Letting the user pick: the Selection strategy”

Sometimes the tenant isn’t in the URL or the token at all — a consultant belongs to three tenants and has to choose. The Selection strategy handles this interactively:

{
"Strategy": "Selection",
"Options": {
"TenantsEndpoint": "https://platform.example.com/api/tenants/selectable"
}
}

When an authenticated user arrives without a .cratis-tenant cookie, AuthProxy calls TenantsEndpoint (with the user’s principal attached) expecting a JSON array of { "id": "...", "name": "..." } objects:

  • One tenant — it’s selected automatically; the cookie is written and the request retried.
  • Several tenants — the proxy injects a short-lived .cratis-tenants cookie with the options and serves the built-in select-tenant.html page. The page submits the choice to /.cratis/select-tenant?tenantId=<id>&returnUrl=<path>, and the proxy validates the selection against the endpoint’s response before writing the .cratis-tenant cookie.

The selection page is one of the overridable pages, so you can brand it.

Optionally verify that a resolved tenant actually exists by pointing AuthProxy at a verification endpoint — {tenantId} is replaced with the resolved id, a 200 means the tenant exists, and anything else sends the user to the tenant-not-found page:

{
"Cratis": {
"AuthProxy": {
"TenantVerification": {
"UrlTemplate": "https://platform.example.com/api/tenants/{tenantId}"
}
}
}
}

A strategy-specific template (like SubHost’s VerificationUrlTemplate above) takes precedence over this global one.

If strategies are configured but none resolves, AuthProxy rejects the request with 401 Unauthorized rather than forwarding it tenant-less — fail closed, not open. There are two deliberate exceptions:

  • With no TenantResolutions configured, requests proceed without a tenant (single-tenant apps don’t need any of this).
  • With a lobby configured and Invite.RedirectToLobbyWhenTenantUnresolved enabled, users without a tenant are redirected to the lobby frontend instead — that’s the onboarding flow.

Strategies run in order until one answers; Host, Claim, and Route go through the tenant registry, SubHost resolves by convention for dynamic SaaS, and Selection asks the user. Verification confirms the tenant is real, and the result rides into your services as Tenant-ID. For the users who don’t have a tenant yet, continue to invites and the lobby.