Tenant Resolution and Multitenancy
TokenIDP is tenant-aware at runtime. OAuth requests, admin APIs, user management, client lookup, token issuance, settings, roles, permissions, MFA policies, and external provider configuration all execute inside a resolved tenant context.
The tenant is not just a UI filter. It is resolved at the request boundary, loaded from the database, stored in request-scoped tenant context, and then used by repositories and business rules to keep tenant data separated.
Tenant Model
Each tenant has a stable TenantKey. The key is the public routing identifier used in URLs, query strings, headers, and issued tenant claims.
Examples:
| Tenant | Tenant key | Production host |
|---|---|---|
| System tenant | system | admin.tokenidp.com or root/default host |
| Tenant B | tenantb | tenantb.tokenidp.com |
| Acme | acme | acme.tokenidp.com |
The system tenant is the platform administration tenant. Operational tenants are customer or organization tenants created through the Admin Portal.
Production Resolution
In production, TokenIDP resolves tenants from the request host.
For a configured root domain:
{
"TenantResolution": {
"AllowedRootDomains": ["tokenidp.com"],
"SystemHostAliases": ["admin.tokenidp.com"],
"DefaultTenant": "system"
}
}
Requests are resolved like this:
| Request host | Resolution |
|---|---|
tenantb.tokenidp.com | tenant key tenantb |
acme.tokenidp.com | tenant key acme |
tokenidp.com | default tenant, usually system |
admin.tokenidp.com | system/default tenant |
unknown-domain.com | rejected |
a.b.tokenidp.com | rejected because nested tenant subdomains are not accepted |
This gives each tenant a clean issuer-facing URL while preserving a shared TokenIDP backend.
Production behavior is intentionally strict. If the host cannot be matched to an allowed root domain, system alias, or valid tenant subdomain, the request is rejected instead of falling back to query string or header resolution.
Development and Localhost Resolution
Local development usually does not have real wildcard DNS such as tenantb.tokenidp.com. For that reason, non-production environments can resolve tenants from query string or header values after host resolution is checked.
Typical development configuration:
{
"TenantResolution": {
"AllowedDevelopmentHosts": ["localhost"],
"DefaultTenant": "system",
"AllowQueryInStaging": true,
"AllowHeaderInStaging": false,
"QueryParameterName": "tenant",
"HeaderName": "X-Tenant-Key"
}
}
With this configuration, local requests can select the tenant using ?tenant=...:
https://localhost:5001/authorize?tenant=tenantb&client_id=tokenidp-admin&...
https://localhost:5001/admin/users?tenant=tenantb
If no tenant is supplied in a non-production environment, TokenIDP falls back to DefaultTenant when configured.
The query-string mode is meant for development, staging, testing, and local portal/API integration. It is not the production routing model.
Optional Header Resolution
Header-based resolution can be enabled in non-production environments:
{
"TenantResolution": {
"AllowHeaderInStaging": true,
"HeaderName": "X-Tenant-Key"
}
}
Example:
curl https://localhost:5001/admin/users \
-H "X-Tenant-Key: tenantb" \
-H "Authorization: Bearer <access-token>"
Header resolution is useful for API tests, service-to-service experiments, and tools where adding a query string is awkward. It should be treated carefully because tenant context is security-sensitive.
Resolution Order
The request resolver follows this order:
- Parse the request host.
- If the host is a tenant subdomain under an allowed root domain, use that tenant key.
- If the host is a root domain or system host alias, use the default tenant.
- In production, reject unresolved hosts.
- In non-production, try query-string tenant resolution when enabled.
- In non-production, try header tenant resolution when enabled.
- If still unresolved, use the configured default tenant.
- If no tenant can be resolved, reject the request.
After a tenant key is resolved, TokenIDP looks up the tenant in the database. The tenant must exist, must not be deleted, and must be active. If the tenant cannot be loaded, the request is rejected.
Request Context
Once resolved, the tenant is stored in request context and made available through the tenant context accessor.
That context is then used by:
- repositories and query filters
- OAuth client lookup
- authorization code and token flows
- user, role, permission, and configuration APIs
- tenant-specific MFA and authentication policy checks
- external identity provider configuration
- token introspection and revocation ownership checks
The context is cleared at the end of the request to avoid leaking tenant state across requests.
Client IDs Across Tenants
Client IDs are unique inside a tenant, not globally. This allows multiple tenants to use the same logical client ID when the tenant context is known.
Example:
| Tenant | Client ID |
|---|---|
system | idp-admin |
tenantb | tokenidp-admin |
acme | tokenidp-admin |
This works because OAuth client resolution is tenant-aware. When a tenant context exists, TokenIDP prefers the client registered under that tenant. The system tenant client can also be available as a fallback for platform-level administration flows.
Avoid relying on duplicate client IDs without a clear tenant context. In production, the tenant should come from the host. In development, include ?tenant=... or an enabled tenant header when testing tenant-specific clients.
Admin Portal Behavior
The Admin Portal should call the backend with a tenant-resolvable URL.
For production:
https://tenantb.tokenidp.com
or a platform/system administration host:
https://admin.tokenidp.com
For local development:
https://localhost:5001?tenant=tenantb
When testing the released React artifacts locally, the static portal can still run from localhost, but the backend API and OAuth authority must point to a URL that lets TokenIDP resolve the intended tenant.
Environment Guidance
Use host-based tenant resolution for production:
- configure
AllowedRootDomains - configure
SystemHostAliases - configure DNS or wildcard DNS for tenant subdomains
- make sure TLS certificates cover the root and tenant subdomains
- avoid query/header tenant selection in production
Use query-string or header resolution for development:
- keep
AllowedDevelopmentHostsset to local hosts such aslocalhost - use
?tenant=tenantbfor browser testing - enable
AllowHeaderInStagingonly when test tooling needs it - keep
DefaultTenantset tosystemfor platform admin flows
Troubleshooting
If a request returns tenant_unavailable, check:
- the host is listed under
AllowedRootDomains,AllowedDevelopmentHosts, orSystemHostAliases - the tenant subdomain maps to an existing
TenantKey - the tenant exists, is active, and is not deleted
- local requests include
?tenant=...when testing a non-default tenant - production DNS sends tenant subdomains to the TokenIDP backend
- the Admin Portal runtime configuration points to the correct authority/API URL
If a request returns too_many_requests, repeated invalid tenant host attempts have triggered the invalid-host throttle window.
Summary
TokenIDP uses strict host-based tenant resolution in production and developer-friendly query/header resolution in non-production. The resolved tenant key becomes the runtime tenant context for OAuth, admin APIs, policies, client lookup, and token ownership checks.
For production, think tenantb.tokenidp.com. For localhost, think https://localhost:5001?tenant=tenantb.