Tenant Resolution

Tenant resolution identifies which tenant a request belongs to. The web module resolves the tenant of each HTTP request with a chain of resolvers, built from the configured resolution modes, and the TenantContextFilter then validates, verifies, and binds it. See Architecture for how the steps fit together.

Resolution Modes

The resolvers of the chain are consulted in order until one finds a tenant identifier. A resolver that finds nothing it handles hands over to the next one, while one that finds tenant information it cannot use rejects the request. See Resolution Contract for the exact rules.

The arconia.multitenancy.resolution.http.resolution-mode property lists the built-in resolvers to use, in the order they are consulted. By default, only the header mode is used.

arconia:
  multitenancy:
    resolution:
      http:
        resolution-mode: subdomain, header

With this configuration, the tenant is read from the subdomain when the host matches the configured pattern, and from the X-TenantId header otherwise.

Mode Source Behaviour in a chain

header

A header of the request.

Hands over when the header is absent. Rejects the request when the header appears more than once.

cookie

A cookie of the request.

Hands over when the cookie is absent. Rejects the request when several cookies carry that name.

path

A segment of the request path.

Hands over when the path does not match the pattern.

subdomain

A label of the request host.

Hands over when the host does not match the pattern.

oauth2

A claim of the OAuth2 token that authenticated the request.

Hands over when the request is not authenticated, or is authenticated by other means than OAuth2. Rejects the request when the OAuth2 token does not carry the claim.

fixed

A fixed tenant identifier.

Always resolves a tenant, so it can only be the last mode and acts as the default tenant.

Mode names are case-insensitive. An empty list, a mode listed twice, a fixed mode that is not last, and a path or subdomain mode without its pattern fail when the application starts. The configured chain is logged once at startup.

Header-Based Resolution

By default, the tenant identifier is resolved from the X-TenantId HTTP header.

  • HTTPie

  • curl

http :8080/api/resource X-TenantId:acme
curl -H "X-TenantId: acme" http://localhost:8080/api/resource

You can customize the header name:

arconia:
  multitenancy:
    resolution:
      http:
        header:
          name: X-Custom-Tenant

A request carrying the header more than once is rejected rather than resolved from the first value, since conflicting values are a sign of a misbehaving or malicious client.

Alternatively, you can resolve the tenant from an HTTP cookie by using the cookie mode.

arconia:
  multitenancy:
    resolution:
      http:
        resolution-mode: cookie

By default, the cookie name is TENANT-ID. You can customize it:

arconia:
  multitenancy:
    resolution:
      http:
        resolution-mode: cookie
        cookie:
          name: MY-TENANT

A request carrying several cookies with that name is rejected, for the same reason as duplicate headers.

Path-Based Resolution

The tenant can be resolved from a segment of the request path, so that /tenants/acme/products serves the tenant acme. Use the path mode and configure a pattern for the request path, where the {tenant} variable captures the tenant identifier.

arconia:
  multitenancy:
    resolution:
      http:
        resolution-mode: path
        path:
          pattern: /tenants/{tenant}/**

The pattern follows the same syntax as Spring MVC request mappings and is matched against the path within the application, so it keeps working when the application is deployed under a context path. The variable may carry an inline regular expression such as /tenants/{tenant:[a-z]+}/**. A request whose path does not match the pattern resolves no tenant.

The resolver only extracts the identifier. It does not rewrite the path, so your handler mappings must still include the tenant segment, for example @GetMapping("/tenants/{tenant}/products"), or a gateway in front of the application must strip it.

Subdomain-Based Resolution

The tenant can also be resolved from the subdomain of the request, so that acme.example.com serves the tenant acme. Use the subdomain mode and configure a pattern for the request host, where the {tenant} variable captures the tenant identifier.

arconia:
  multitenancy:
    resolution:
      http:
        resolution-mode: subdomain
        subdomain:
          pattern: "{tenant}.example.com"

The pattern follows the same syntax as Spring MVC request mappings, with the dot as the separator, and is matched case-insensitively against the whole host. The variable may be embedded in a label, so {tenant}-admin.example.com serves the tenant acme at acme-admin.example.com, and it may carry an inline regular expression such as {tenant:[a-z0-9-]+}.example.com. A request whose host does not match the pattern resolves no tenant. The variable matches a single label, so nested subdomains such as api.acme.example.com never match the {tenant}.example.com pattern.

Host names are case-insensitive, so the host is lowercased before matching, and the resolved tenant identifier is always lowercase: Acme.example.com and acme.example.com both serve the tenant acme. Register tenants with lowercase identifiers when using this mode, because a tenant such as Acme can never be matched from a subdomain. This is the only mode that normalizes case. Identifiers resolved from headers, cookies, paths and tokens are used as they are.

During local development, you can use the {tenant}.localhost pattern. Most browsers resolve acme.localhost to the loopback address without any extra setup, so http://acme.localhost:8080 serves the tenant acme.

The host is read from the request as seen by the application. When the application runs behind a reverse proxy or load balancer, configure Spring Boot to process forwarded headers, for example, with server.forward-headers-strategy=framework, so that the original host from X-Forwarded-Host is used rather than the address of the proxy.

OAuth2-Based Resolution

When the application is secured with OAuth2, the tenant can be resolved from a claim in the token that authenticated the request.

arconia:
  multitenancy:
    resolution:
      http:
        resolution-mode: oauth2

This works both for OAuth2 client applications, where the claim is read from the ID token and the UserInfo response, and for OAuth2 resource servers, where it is read from the access token, whether it is a JWT or an opaque token resolved via introspection. Because the claim is read from the authenticated principal rather than from the raw Authorization header, it also works for client applications that carry a session instead of a bearer token. The mode is named after what it reads, an OAuth2 principal, rather than one token format.

It requires Spring Security OAuth2 on the classpath, for example, via the spring-boot-starter-security-oauth2-resource-server or spring-boot-starter-security-oauth2-client dependency. The application fails to start otherwise.

By default, the claim name is tenant_id. You can customize it:

arconia:
  multitenancy:
    resolution:
      http:
        resolution-mode: oauth2
        oauth2:
          claim-name: tid

In a chain, this mode hands over to the next resolver when the request is not authenticated, which lets a login page be served by tenant from the subdomain while authenticated requests take the tenant from the token. Once a request is authenticated with an OAuth2 token, a missing claim rejects the request instead of handing over, so that a client can never make the application fall back to a header it controls.

This mode requires the request to be authenticated before the tenant is resolved, so the TenantContextFilter must run after the Spring Security filter chain. That is the default. See OAuth2 Mode under Filter Placement if you change the filter placement. A permitAll endpoint that is not among the ignored paths is reached by anonymous requests, which the oauth2 mode alone cannot resolve, so either ignore the path or add another mode to the chain.

Fixed Tenant and Default Tenant

The fixed mode resolves the tenant configured in arconia.multitenancy.resolution.fixed.tenant-identifier, regardless of the request. On its own, it turns the application into a single-tenant deployment:

arconia:
  multitenancy:
    resolution:
      http:
        resolution-mode: fixed
      fixed:
        tenant-identifier: acme

As the last mode of a chain, it acts as the default tenant, used only when no other resolver found a tenant in the request:

arconia:
  multitenancy:
    resolution:
      http:
        resolution-mode: header, fixed
      fixed:
        tenant-identifier: public

A request rejected by an earlier resolver, for example, because it carries two X-TenantId headers, never reaches the fixed tenant. Without a fixed mode, a request without a tenant is rejected, which is the safe default for an application that must never serve data without knowing the tenant.

The fixed identifier goes through the same validation at startup and verification on each request as any other tenant, so it must be a known, enabled tenant when a TenantDetailsService is configured.

Custom Tenant Resolution

The configured modes are assembled into a single HttpRequestTenantResolver bean. Define your own bean of that type to replace it entirely, following the usual Spring Boot convention: the auto-configuration backs off.

import io.arconia.multitenancy.web.resolution.HttpRequestTenantResolver;

@Bean
HttpRequestTenantResolver tenantResolver() {
    return request -> request.getParameter("tenant");
}

To combine your own strategy with the built-in ones, compose them explicitly with a CompositeHttpRequestTenantResolver, which consults its resolvers in order:

import io.arconia.multitenancy.web.resolution.CompositeHttpRequestTenantResolver;
import io.arconia.multitenancy.web.resolution.HeaderTenantResolver;
import io.arconia.multitenancy.web.resolution.HttpRequestTenantResolver;

@Bean
HttpRequestTenantResolver tenantResolver() {
    HttpRequestTenantResolver queryParameterTenantResolver = request -> request.getParameter("tenant");
    return new CompositeHttpRequestTenantResolver(List.of(
        queryParameterTenantResolver,
        HeaderTenantResolver.builder().build()));
}

The resolution-mode property and the mode-specific properties have no effect once you define your own resolver bean, since the built-in resolvers are then created by your code.

Resolution Contract

A custom resolver implements HttpRequestTenantResolver, the specialization for HttpServletRequest of the core TenantResolver<T> interface:

@FunctionalInterface
public interface TenantResolver<T> {

    @Nullable
    String resolveTenantIdentifier(T source);

}

A resolver has three possible outcomes:

  • It returns a tenant identifier when the source carries one.

  • It returns null when the source carries nothing this resolver handles, so that the next resolver in the chain is consulted.

  • It throws an InvalidTenantIdentifierException when the source carries tenant information that is present but unusable, such as conflicting values or an authenticated principal without a tenant claim. No further resolver is consulted, so that unusable input from one source never falls through to a less trusted one.

Only null hands over to the next resolver. A blank value is a value that was found: it stops the chain too and is then rejected by the identifier validation. That is why an empty tenant header, or an OAuth2 token whose tenant claim is empty, never falls back to a less trusted mode.

Exception messages are logged and returned to the caller, so describe the rule that was violated rather than embedding the raw request value.

Trusting the Resolved Tenant

In the header, cookie, path and subdomain modes the tenant identifier is client input. Validation proves that it is well-formed, and verification proves that the tenant exists and is enabled. Neither proves that the caller belongs to that tenant. With a shared identity provider, an authenticated user of tenant acme can send X-TenantId: beans and be served as beans unless the application checks that the tenant in the token matches the tenant in the context. Until Arconia provides that check, do it in your authorization rules whenever the tenant does not come from the token itself.

With one identity provider per tenant, the check is implicit: the tenant resolved from the request selects the trusted issuer, and a token minted for another tenant does not validate against its keys. A tenant identifier resolved before authentication is safe to use for selecting a trusted issuer, as long as it is resolved through a TenantDetailsService so that only known tenants are accepted and never used to build an issuer URL directly.

The oauth2 mode reads the tenant from the verified token, so the identity and the tenant come from the same trusted source.

Configuration Properties

Table 1. HTTP Tenant Resolution Configuration Properties
Property Default Description

arconia.multitenancy.resolution.http.enabled

true

Whether HTTP tenant resolution is enabled. When disabled, neither the resolvers nor the filter are configured.

arconia.multitenancy.resolution.http.resolution-mode

header

Modes of HTTP resolution, consulted in order until one resolves a tenant. Options: header, cookie, path, subdomain, oauth2, fixed. The fixed mode always resolves a tenant, so it can only be the last one.

arconia.multitenancy.resolution.http.header.name

X-TenantId

Name of the HTTP header from which to resolve the current tenant.

arconia.multitenancy.resolution.http.cookie.name

TENANT-ID

Name of the HTTP cookie from which to resolve the current tenant.

arconia.multitenancy.resolution.http.path.pattern

Pattern matched against the request path, containing a {tenant} variable that captures the tenant identifier, such as /tenants/{tenant}/**. Required when the path mode is used.

arconia.multitenancy.resolution.http.subdomain.pattern

Pattern matched against the request host, containing a {tenant} variable that captures the tenant identifier, such as {tenant}.example.com. Required when the subdomain mode is used.

arconia.multitenancy.resolution.http.oauth2.claim-name

tenant_id

Name of the OAuth2 token claim from which to resolve the current tenant.

arconia.multitenancy.resolution.fixed.tenant-identifier

default

Identifier of the tenant resolved by the fixed mode.