Skip to main content
Version: v0.25.0 (Latest)

Tenant Resolution

Every request that lands on an EDK service is matched to a tenant before anything tenant-scoped runs. Resolution happens early in request handling; once a tenant is determined, the request descends into that tenant's scope and every tenant-bound read, write, and signing key sees the right tenant.

Four signals can identify a tenant, and they are tried in a fixed priority order. The first signal that resolves wins.

EDK tenant resolution order

The Resolution Order

1. Bearer JWT

For any authenticated call (admin REST, OID4VCI /credential, OID4VP /direct_post with a wallet token, AS-issued tokens carrying the tenant claim), the bearer JWT's tenant_id claim is authoritative. When present it is used immediately and no other signal is consulted. The JWT is the only signal that proves the caller is acting on behalf of the tenant; the other three say where the request landed, not who the caller is.

The X-Tenant-Id header is informational only. The resolver never consults it to determine a tenant. Logs and traces may carry it for diagnostics, but it does not influence routing or authorization. Treating it as authoritative would let any caller act as any tenant they name.

2. Custom Domain

The incoming Host header is looked up against the tenant's verified custom domains. A verified wallet.acme.com resolves to its tenant. Unverified custom domains are skipped, so a tenant can register a domain in advance and keep the record while the DNS verification challenge completes, without the resolver routing traffic to a host the tenant does not yet control. Custom domain takes priority over platform subdomain.

3. Platform Subdomain

The host is parsed as <labels>.<platform-base>, where <platform-base> is the configured tenant.resolution.platform_base_host. The label immediately to the left of the platform base is the tenant slug. Two shapes resolve:

  • <tenant>.<platform-base>: acme.saas.com resolves to acme.
  • <service>.<tenant>.<platform-base>: a service-specific label (issuer, verifier, auth, did) may sit to the left of the slug without changing the result. issuer.acme.saas.com resolves to acme.

4. Path Slug

For the protocol surfaces that carry the tenant slug in the URL path (/{tenant}/oid4vci/..., /{tenant}/oid4vp/..., /{tenant}/.well-known/openid-configuration), the leading path segment is taken as the tenant slug.

Admin endpoints are a special case: a {tenantId} in an admin path (such as /tenants/{tenantId}/domains) is the operation target, not the acting tenant. The acting tenant still comes from the JWT, and a JWT bound to tenant A asking to mutate tenant B is rejected by the cross-tenant authorization check.

Well-Known URL Forms

The metadata endpoints support more than one URL form because clients in the field use both the spec form and the legacy slug-before form:

  • OID4VCI issuer metadata. Spec form /.well-known/openid-credential-issuer/{tenant-slug}; legacy form /{tenant-slug}/.well-known/openid-credential-issuer. Both point at the same tenant-aware endpoint.
  • OAuth AS metadata. Spec form /.well-known/oauth-authorization-server/{tenant-slug}; legacy form available. The slug insertion follows RFC 8414.
  • OIDC Discovery. /{tenant-slug}/.well-known/openid-configuration (slug-before is the OIDC spec form; clients do not expect a slug-after form).

The URLs a service advertises (the credential_issuer in OID4VCI metadata, the issuer in OAuth metadata, the request_uri_base in an OID4VP authorization request) come from the tenant's public-endpoint bindings, not from the request host. Resolution determines which tenant a request belongs to; the public-endpoint binding determines what URLs that tenant advertises. See Domains and Public Endpoints.

Resolution Within a Hierarchy

A request that matches a child tenant's slug or domain resolves to the child, never the parent. There is no automatic walk-up: acme-nl.saas.com resolves to acme-nl regardless of its parent.

When no signal resolves (the host matches no domain or subdomain, the path carries no slug, and the JWT carries no tenant claim) the behaviour depends on the surface:

  • A public protocol endpoint bound to a deployment-wide URL (the application tenant's root AS metadata form, for example) treats the absence of a tenant as the application tenant.
  • Other public protocol endpoints refuse the call with a 400.
  • Admin REST endpoints refuse the call with a 401 (the JWT is missing or invalid).

There is no implicit fallback to a default tenant. A request that fails to resolve is rejected explicitly.

The Fail-Closed Advertising Rule

When a service needs to advertise a URL for the resolved tenant and service type but no public-endpoint binding exists, it refuses to advertise anything rather than falling back to the request host. In a production deployment the request host is often the cluster's internal host or the CDN's origin host, neither of which a wallet can reach. A development deployment may opt into host fallback (see Domains and Public Endpoints); the published configuration keeps the fail-closed default.

Caching and Propagation

Resolution results are cached in each service replica so a subdomain or path-slug lookup does not hit the database on every request. Unknown slugs and suspended tenants are cached too, so a flood of unknown-slug probes does not generate one database query per probe.

A routing change on one replica (a new tenant, a status change, a slug rename, a domain verification) reaches every replica's cache without a restart: the change propagates over the shared event channel and each replica invalidates the affected entry. A cache TTL is the safety net for a missed notification. Tune it with tenant.resolution.cache_ttl_seconds; the default is conservative because event propagation, not the TTL, is the primary invalidation path.

Resolution Settings

The resolver reads four deployment-scoped keys at startup (these are app-scope, not per-tenant, because resolution runs before any tenant is in scope):

  • tenant.resolution.platform_base_host. The host suffix subdomain resolution treats as the platform base. Required when platform subdomain resolution is enabled.
  • tenant.resolution.platform_subdomain_enabled. Toggles platform-subdomain resolution. Typically on; off for deployments that use only custom domains.
  • tenant.resolution.trusted_proxy_hop_count. How many X-Forwarded-Host hops to trust behind reverse proxies and CDNs.
  • tenant.resolution.cache_ttl_seconds. The fallback cache TTL.

Common Mistakes

  • Trusting X-Tenant-Id. It is informational only; the JWT claim is the authority.
  • Falling back to the request host for advertised URLs. When a tenant resolves, the service advertises the URLs bound for that tenant. Falling back to the request host produces metadata a wallet cannot reach behind a CDN or custom domain.
  • Mixing up the order. Custom domain before platform subdomain before path slug; the JWT runs first whenever present.
  • Resolving with the cluster's internal hostname. An internal call from one service to another (the issuer calling the AS, for example) should carry the host a wallet would see, not the cluster's own host.