Skip to content

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

  1. Tenant/project ownership and memberships remain source-of-truth for authorization.
  2. Authentication protocol details (OIDC/SAML) must not bypass tenant isolation.
  3. Client-supplied tenant context is advisory only; server decides effective tenant.
  4. Callback state is single-use and tenant/provider bound.
  5. 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

  1. Preferred: verified domain -> (org_id, idp_id) mapping.
  2. Fallback: explicit tenant_hint validated against configured federation mappings.
  3. tenant_hint format for authorize contract: tenant slug (organizations.slug) using regex ^[a-z0-9][a-z0-9-]{1,62}$.
  4. No valid mapping -> reject auth request (invalid_request).
  5. Resolved tenant in state must match callback validation tenant.

Security Controls (Non-Negotiable)

  1. State handling
  2. opaque random state id + server-side stored record
  3. TTL-limited and single-use consume
  4. includes org_id and provider binding
  5. baseline TTL: 600 seconds
  6. planned policy key (not yet seeded): auth.federation_state_ttl_seconds
  7. Issuer validation
  8. OIDC iss exact match for selected provider
  9. SAML entity-id exact match for selected provider
  10. Replay prevention
  11. one-time state consume
  12. nonce/jti replay guard where protocol applies
  13. Secret custody
  14. provider secrets and SAML cert material stored as encrypted envelope fields (*_enc) using the canonical envelope from doc/architecture/Encryption_Envelope_Spec.md (AES-256-GCM, key-id tracked, rotation model defined)
  15. Audit
  16. all federation config mutations and auth failures log canonical correlation id and target org_id.

Threat Cases and Mitigations

  1. Issuer spoofing
  2. Attack: attacker uses valid token/assertion from another issuer.
  3. Mitigation: tenant-scoped exact issuer/entity-id allowlist.
  4. Cross-tenant token replay
  5. Attack: callback artifact from tenant A replayed in tenant B flow.
  6. Mitigation: state bound to org_id + provider id + single-use consume.
  7. Callback-state tampering
  8. Attack: attacker modifies state to alter tenant/provider binding.
  9. Mitigation: opaque server-stored state and strict consume-time validation.
  10. Forged tenant hint
  11. Attack: client submits arbitrary tenant hint to pivot tenant context.
  12. Mitigation: hint not trusted without server mapping and provider validation.
  13. Confused-deputy via shared callback URL
  14. Attack: callback intended for one provider/tenant context is replayed through another.
  15. Mitigation: callback consumes state bound to (org_id, provider_id, provider_type) and rejects any mismatch.

Planned Policy Keys

  1. auth.federation_state_ttl_seconds (default 600)
  2. 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)

  1. Auth authorize/exchange endpoints may accept optional tenant federation hints.
  2. OIDC and SAML use protocol-specific authorize/callback endpoints in contract:
  3. /api/v1/auth/oidc/authorize, /api/v1/auth/oidc/exchange
  4. /api/v1/auth/saml/authorize, /api/v1/auth/saml/callback
  5. Federation config schemas (TenantFederationProvider, TenantFederationDomainBinding) are pre-declared for contract readiness; tenant-admin CRUD endpoint binding is deferred.
  6. 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.md
  • doc/architecture/Tenant_Project_Ownership_Baseline.md
  • doc/architecture/Service_Account_Model.md
  • doc/api/openapi.draft.yaml