पाठ 7 / 32

Resolving the Tenant for a Request

यह पाठ अभी हिंदी में अनुवादित नहीं है, इसलिए अंग्रेज़ी में दिखाया जा रहा है।

Identify the tenant of every incoming request from subdomains, custom domains, paths or token claims, and reject requests whose tenant cannot be resolved.

लेखक: · 15 साल से वेब डेवलपर

Where does the tenant come from?

Before any business logic runs, the system must answer: which tenant is this request for? Common sources are the subdomain (zen.example-saas.com), a custom domain the tenant mapped to you (app.acme.in), a path prefix (/t/zen/projects), an explicit header set by a trusted gateway, or a claim inside the access token issued at login. Hostnames and paths say which tenant the caller wants; the authenticated token says which tenants the user is allowed into. Robust systems use both: resolve the requested tenant from the URL, then check that the signed-in user has a membership in it. Resolution should happen once, at the edge (gateway or middleware), and fail closed: if no tenant can be resolved or the user is not a member, return an error instead of falling back to some default tenant.

The tenant resolution pipeline

A request is received, the host or path gives the requested tenant, the token proves membership, and only then is a tenant context attached for the rest of the request.

Four stages: incoming request, tenant lookup from host or path, membership check against the token, tenant context attached to the request.
Figure 3.1: request -> resolve tenant from host/path -> verify membership from token -> set tenant context

Resolving from the host name

Custom domains are checked first, then subdomains of the product's root domain. Anything else is rejected.

CUSTOM_DOMAINS = {"app.acme.in": "acme"}
ROOT = "example-saas.com"

def resolve_tenant(host: str) -> str:
    host = host.split(":")[0].lower()
    if host in CUSTOM_DOMAINS:
        return CUSTOM_DOMAINS[host]
    if host.endswith("." + ROOT):
        sub = host[: -len(ROOT) - 1]
        if sub and "." not in sub:
            return sub
    raise LookupError(f"unknown tenant for host {host!r}")

for h in ["zen.example-saas.com", "app.acme.in:443", "evil.com"]:
    try:
        print(h, "->", resolve_tenant(h))
    except LookupError as e:
        print(h, "->", e)

Output:

zen.example-saas.com -> zen
app.acme.in:443 -> acme
evil.com -> unknown tenant for host 'evil.com'

Tracing two lookups

Follow the highlighted line for a valid subdomain and then for an unknown host.

CUSTOM_DOMAINS = {"app.acme.in": "acme"}
ROOT = "example-saas.com"

def resolve_tenant(host: str) -> str:
    host = host.split(":")[0].lower()
    if host in CUSTOM_DOMAINS:
        return CUSTOM_DOMAINS[host]
    if host.endswith("." + ROOT):
        sub = host[: -len(ROOT) - 1]
        if sub and "." not in sub:
            return sub
    raise LookupError(f"unknown tenant for host {host!r}")

for h in ["zen.example-saas.com", "app.acme.in:443", "evil.com"]:
    try:
        print(h, "->", resolve_tenant(h))
    except LookupError as e:
        print(h, "->", e)
  1. Line 14: The loop starts with the subdomain host. Nothing has been resolved yet.
  2. Line 5: The port is stripped and the host lower-cased, because DNS names are case-insensitive and ports are irrelevant to tenancy.
  3. Line 6: Custom domains are checked first so a tenant with its own domain is always matched exactly. This host is not one.
  4. Line 8: The host does end with the product's root domain, so it may be a tenant subdomain.
  5. Line 9: Slicing removes '.example-saas.com', leaving the label in front of it.
  6. Line 10: A nested label like 'a.b' is refused, so only a single subdomain level can name a tenant.
  7. Line 11: The tenant id is returned and printed.
  8. Line 5: Later in the loop, 'evil.com' is normalised the same way.
  9. Line 8: It is not a custom domain and does not end with the root domain, so both branches are skipped.
  10. Line 12: The function fails closed: it raises instead of guessing a default tenant.
  11. Line 18: The caller catches the error and reports it; a web app would return 404 or 400 here.

Never default to a tenant

Code like tenant = resolve(host) or 'default' turns a typo or a scanner into access to a real tenant's data. Unknown tenant means an error response, every time.

त्वरित जाँच: A request arrives for `acme.example-saas.com` with a valid token for a user who belongs only to the tenant `zen`. What should happen?

  • Serve acme's data, since the hostname is valid
  • Serve zen's data, since that is the user's tenant
  • Silently redirect the user to a random tenant
  • Reject the request, because the user has no membership in acme
Answer

Reject the request, because the user has no membership in acme — The hostname names the requested tenant; the token proves which tenants the user may access. A mismatch must fail closed with an authorisation error.