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:

TenantTenant keyProduction host
System tenantsystemadmin.tokenidp.com or root/default host
Tenant Btenantbtenantb.tokenidp.com
Acmeacmeacme.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 hostResolution
tenantb.tokenidp.comtenant key tenantb
acme.tokenidp.comtenant key acme
tokenidp.comdefault tenant, usually system
admin.tokenidp.comsystem/default tenant
unknown-domain.comrejected
a.b.tokenidp.comrejected 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:

  1. Parse the request host.
  2. If the host is a tenant subdomain under an allowed root domain, use that tenant key.
  3. If the host is a root domain or system host alias, use the default tenant.
  4. In production, reject unresolved hosts.
  5. In non-production, try query-string tenant resolution when enabled.
  6. In non-production, try header tenant resolution when enabled.
  7. If still unresolved, use the configured default tenant.
  8. 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:

TenantClient ID
systemidp-admin
tenantbtokenidp-admin
acmetokenidp-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 AllowedDevelopmentHosts set to local hosts such as localhost
  • use ?tenant=tenantb for browser testing
  • enable AllowHeaderInStaging only when test tooling needs it
  • keep DefaultTenant set to system for platform admin flows

Troubleshooting

If a request returns tenant_unavailable, check:

  • the host is listed under AllowedRootDomains, AllowedDevelopmentHosts, or SystemHostAliases
  • 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.