ADR-002: Multi-Tenant Selection With Same API Key
Status: Accepted Date: 2026-02-22 Sprint: SPRINT_20260222_053_DOCS_multi_tenant_same_api_key_contract_baseline.md
This record defines the canonical contract — claims, client metadata, headers, and error semantics — for clients that belong to more than one tenant while reusing a single API key/client registration. Read it before touching tenant resolution in Authority, the gateway, or any downstream service.
Context
Stella Ops must support clients that are assigned to more than one tenant while still using the same API key/client registration. Existing behavior assumes a scalar tenant assignment (tenant) and cannot safely select among multiple tenant memberships.
Without a canonical contract, modules diverge on claim names, header behavior, default selection, and mismatch handling. This creates cross-tenant leakage risk and migration churn.
Decision
Use one selected tenant per access token, chosen at token issuance time.
Selected model (accepted)
- Client metadata supports both:
tenant(scalar compatibility/default tenant)tenants(space-delimited assignment set; normalized lowercase, unique, sorted)
- Token request may include
tenant=<id>. - Authority resolves selected tenant deterministically:
- If
tenantparameter is present: it must exist in assigned tenants. - If no parameter:
- use scalar
tenantwhen configured, otherwise - use single-entry
tenants, otherwise - reject as ambiguous.
- use scalar
- If
- Issued tokens carry:
stellaops:tenant(selected tenant)stellaops:allowed_tenants(space-delimited assigned set, optional)
- Gateway and services continue operating with one effective tenant per request.
Fallback model (rejected for default path)
Multi-tenant token + per-request header override (X-StellaOps-TenantId) as primary selector.
Reason rejected:
- Increases header spoofing and token confusion risk.
- Creates inconsistent downstream behavior where services interpret tenant from different sources.
- Expands change surface across all modules immediately.
Canonical Contract
Claims
stellaops:tenant: selected tenant for this token.stellaops:allowed_tenants: assigned tenant set (space-delimited, sorted).
Client metadata
tenant: scalar assignment / deterministic default.tenants: assigned set (space-delimited).
Headers
- Canonical tenant header:
X-StellaOps-TenantId. - Legacy/raw tenant header:
X-Tenant-Id— accepted only for bounded migration use and stripped at the gateway ingress (see Threat Model below); never trusted as a selector.
Error semantics
- Requested tenant not assigned: reject
invalid_request. - Missing tenant for tenant-required scope: reject
invalid_client/invalid_requestdepending on grant validation stage. - Ambiguous tenant selection (multi-assigned, no default, no request): reject
invalid_request. - Token tenant not in client assignments during validation: reject
invalid_token.
Threat Model
Header spoofing
Risk: caller supplies tenant headers to escalate into another tenant.
Mitigation: gateway strips inbound identity headers and rewrites from validated claims.
Token confusion
Risk: token tenant differs from issuing client assignment or persisted token document.
Mitigation: validation enforces principal/document/client assignment consistency.
Cross-tenant leakage
Risk: silent default tenant fallback routes tenant-scoped requests incorrectly.
Mitigation: remove authenticated "default" tenant fallback and fail ambiguous selections.
Consequences
Positive
- Keeps downstream service model stable: one tenant per token/request.
- Enables same-key multi-tenant clients without global API contract break.
- Supports UI hydration with explicit assigned tenant set.
Tradeoff
- Tenant switching requires requesting a token for the target tenant.
- Multi-assigned clients without explicit default must send
tenantduring token issuance.
References
docs/implplan/SPRINT_20260222_053_DOCS_multi_tenant_same_api_key_contract_baseline.mddocs/implplan/SPRINT_20260222_054_Authority_same_key_multi_tenant_token_selection.mddocs/implplan/SPRINT_20260222_055_Router_tenant_header_enforcement_and_selection_flow.md
