Tenant Federation SSO Model (OIDC/SAML)¶
Purpose¶
Define the architecture-first baseline for per-tenant enterprise SSO federation.
This document covers: - tenant federation configuration model, - login/callback resolution flow, - membership binding expectations, - threat model and required controls.
Scope¶
In scope: - per-tenant identity-provider configuration contract - tenant resolution and callback-state model - issuer validation and replay defenses - authn/authz boundary with tenant/project memberships
Out of scope: - full admin UX implementation - SCIM/group sync lifecycle automation - production-specific IdP vendor playbooks
Design Principles¶
- Tenant/project ownership and memberships remain source-of-truth for authorization.
- Authentication protocol details (OIDC/SAML) must not bypass tenant isolation.
- Client-supplied tenant context is advisory only; server decides effective tenant.
- Callback
stateis single-use and tenant/provider bound. - Issuer validation is exact-match and tenant-scoped.
Schema Contract (Accepted Baseline; Implementation Pending Migration PR)¶
tenant_identity_providers¶
tenant_identity_providers (
id uuid primary key,
org_id uuid not null references organizations(id),
provider_type text not null check (provider_type in ('oidc','saml')),
provider_name text not null,
enabled boolean not null default false,
-- OIDC fields (nullable for SAML rows)
issuer text null,
client_id text null,
client_secret_enc jsonb null,
authorization_endpoint text null,
token_endpoint text null,
jwks_uri text null,
-- SAML fields (nullable for OIDC rows)
saml_entity_id text null,
saml_sso_url text null,
saml_x509_cert_enc jsonb null,
created_by_user_id uuid not null references users(id),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
deleted_at timestamptz null
)
Constraints:
1. unique (org_id, provider_name) on active (non-deleted) rows.
2. unique (org_id, issuer) for active OIDC providers.
3. unique (org_id, saml_entity_id) for active SAML providers.
4. soft-delete only (audit continuity).
tenant_federation_domain_bindings¶
tenant_federation_domain_bindings (
id uuid primary key,
org_id uuid not null references organizations(id),
domain text not null, -- normalized lower-case FQDN
idp_id uuid not null references tenant_identity_providers(id),
verification_state text not null check (verification_state in ('pending','verified','failed')),
created_by_user_id uuid not null references users(id),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
deleted_at timestamptz null
)
Constraints:
1. unique (domain) on active rows.
2. idp_id must belong to same org_id as binding row.
Domain Verification Lifecycle¶
verification_state transitions:
1. pending -> verified
- Trigger: successful domain proof validation (DNS TXT challenge as default method).
2. pending -> failed
- Trigger: verification attempt fails or times out.
3. failed -> pending
- Trigger: tenant admin re-initiates verification.
4. verified -> pending
- Trigger: binding/domain changes requiring re-verification (for example, domain transfer or binding update).
failed is not terminal; retry path is required.
Login and Callback Flow¶
1) Client initiates authorize
OIDC: GET /api/v1/auth/oidc/authorize?tenant_hint=<tenant-slug>&redirect_uri=...&code_challenge=...
SAML: GET /api/v1/auth/saml/authorize?tenant_hint=<tenant-slug>&redirect_uri=...
- SAML `redirect_uri` is post-auth destination after ACS completion (not the ACS endpoint).
2) API resolves candidate federation context
- via verified domain binding OR validated tenant hint
- selects tenant IdP config (OIDC or SAML mapped flow)
3) API mints opaque state (single-use, TTL)
state payload bind:
- OIDC state: { org_id, provider_id, provider_type, redirect_uri, code_challenge_hash, issued_at, nonce }
- SAML state: { org_id, provider_id, provider_type, redirect_uri, issued_at, nonce }
(`code_challenge_hash` is OIDC-only)
4) User authenticates at external IdP and returns to callback
- OIDC: code callback exchanged by POST /api/v1/auth/oidc/exchange
- SAML: assertion callback POST /api/v1/auth/saml/callback (ACS) using
standard form fields `SAMLResponse` and `RelayState`
- SAML `RelayState` carries the `state` token issued by `/api/v1/auth/saml/authorize`
5) API exchange/consume
- validate state exists, unexpired, single-use, and binding matches callback context
- validate issuer/entity-id against tenant provider config
- resolve or provision local user identity mapping
6) Membership gate
- verify active tenant membership for resolved user/org
- for JIT-provisioned identities, membership must be created atomically only when
a valid pre-authorization/invite record exists; no implicit auto-membership grant
- determine project context per API rules (request-scoped project)
7) Issue GPUaaS session/token
- include org_id claim
- authorization decisions remain membership/policy based
Tenant Resolution Rules¶
- Preferred: verified domain -> (
org_id,idp_id) mapping. - Fallback: explicit
tenant_hintvalidated against configured federation mappings. tenant_hintformat for authorize contract: tenant slug (organizations.slug) using regex^[a-z0-9][a-z0-9-]{1,62}$.- No valid mapping -> reject auth request (
invalid_request). - Resolved tenant in state must match callback validation tenant.
Security Controls (Non-Negotiable)¶
- State handling
- opaque random state id + server-side stored record
- TTL-limited and single-use consume
- includes
org_idand provider binding - baseline TTL: 600 seconds
- planned policy key (not yet seeded):
auth.federation_state_ttl_seconds - Issuer validation
- OIDC
issexact match for selected provider - SAML entity-id exact match for selected provider
- Replay prevention
- one-time state consume
- nonce/jti replay guard where protocol applies
- Secret custody
- provider secrets and SAML cert material stored as encrypted envelope fields (
*_enc) using the canonical envelope fromdoc/architecture/Encryption_Envelope_Spec.md(AES-256-GCM, key-id tracked, rotation model defined) - Audit
- all federation config mutations and auth failures log canonical correlation id and target
org_id.
Threat Cases and Mitigations¶
- Issuer spoofing
- Attack: attacker uses valid token/assertion from another issuer.
- Mitigation: tenant-scoped exact issuer/entity-id allowlist.
- Cross-tenant token replay
- Attack: callback artifact from tenant A replayed in tenant B flow.
- Mitigation: state bound to
org_id+ provider id + single-use consume. - Callback-state tampering
- Attack: attacker modifies state to alter tenant/provider binding.
- Mitigation: opaque server-stored state and strict consume-time validation.
- Forged tenant hint
- Attack: client submits arbitrary tenant hint to pivot tenant context.
- Mitigation: hint not trusted without server mapping and provider validation.
- Confused-deputy via shared callback URL
- Attack: callback intended for one provider/tenant context is replayed through another.
- Mitigation: callback consumes state bound to (
org_id,provider_id,provider_type) and rejects any mismatch.
Planned Policy Keys¶
auth.federation_state_ttl_seconds(default 600)auth.federation_domain_verify_timeout_seconds(default 900)
Note:
- These keys are design-planned and not yet present in scripts/seed.sql /
doc/architecture/Seed_Data_Spec.md.
Contract Implications (OpenAPI Draft)¶
- Auth authorize/exchange endpoints may accept optional tenant federation hints.
- OIDC and SAML use protocol-specific authorize/callback endpoints in contract:
/api/v1/auth/oidc/authorize,/api/v1/auth/oidc/exchange/api/v1/auth/saml/authorize,/api/v1/auth/saml/callback- Federation config schemas (
TenantFederationProvider,TenantFederationDomainBinding) are pre-declared for contract readiness; tenant-admin CRUD endpoint binding is deferred. - Future admin federation endpoints must be tenant-admin protected and audit-logged.
Authorization Boundary Reminder¶
Federation only authenticates identity and resolves tenant context candidate.
Authorization remains server-side:
- tenant membership (tenant_memberships)
- project membership (project_memberships)
- policy chain (global -> tenant -> project)
No federation signal should grant resource access without membership checks.
References¶
doc/architecture/adrs/ADR-010-tenant-federation-sso-model.mddoc/architecture/Tenant_Project_Ownership_Baseline.mddoc/architecture/Service_Account_Model.mddoc/api/openapi.draft.yaml