# 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` ## 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) 1. Client metadata supports both: - `tenant` (scalar compatibility/default tenant) - `tenants` (space-delimited assignment set; normalized lowercase, unique, sorted) 2. Token request may include `tenant=`. 3. Authority resolves selected tenant deterministically: - If `tenant` parameter is present: it must exist in assigned tenants. - If no parameter: - use scalar `tenant` when configured, otherwise - use single-entry `tenants`, otherwise - reject as ambiguous. 4. Issued tokens carry: - `stellaops:tenant` (selected tenant) - `stellaops:allowed_tenants` (space-delimited assigned set, optional) 5. 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-Tenant`) 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-Tenant`. - Legacy compatibility header: `X-Stella-Tenant` (bounded migration use only). ### Error semantics - Requested tenant not assigned: reject `invalid_request`. - Missing tenant for tenant-required scope: reject `invalid_client` / `invalid_request` depending 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 `tenant` during token issuance. ## References - `docs/implplan/SPRINT_20260222_053_DOCS_multi_tenant_same_api_key_contract_baseline.md` - `docs/implplan/SPRINT_20260222_054_Authority_same_key_multi_tenant_token_selection.md` - `docs/implplan/SPRINT_20260222_055_Router_tenant_header_enforcement_and_selection_flow.md`