# Journey: Tenant SSO configuration — SAML / OIDC + claim mapping + auth policy

This is a split-surface journey, not one tenant-admin wizard. A platform
operator creates, imports, edits, probes, and deletes connections in
`/operator/sso`. A tenant admin can only read the resulting tenant-scoped roster
in `/identity`; the policy cards below that roster run fixed sample inputs. OIDC
has a live SP-initiated login and callback. SAML can be configured and probed,
but SAML login is explicitly deferred.

| Capability                 | Shipped boundary                                                               |
| -------------------------- | ------------------------------------------------------------------------------ |
| SAML metadata              | Import, persist, edit, delete, and reachability probe                          |
| OIDC discovery             | Import, persist, edit, probe, and live SP-initiated login                      |
| Claim mapping              | Seven internal slots and four transforms, applied by OIDC callback             |
| Tenant-admin identity page | Tenant-filtered read-only roster plus sample policy evaluations                |
| Auth policy                | Pure evaluator and fixed sample only; no tenant policy store or editor         |
| Enablement                 | No draft/probed/enabled state; a valid saved connection has no activation gate |

## Personas

- **Platform SSO operator** — holds `admin:*` or
  `admin:workspace:tenant-console` and owns the live `/operator/sso` editor.
- **Tenant admin** — holds a tenant-admin session and inspects only the
  connections for that session's tenant at `/identity`.
- **OIDC identity provider** — supplies discovery and JWKS documents, then posts
  a signed `id_token` to the callback.
- **SAML identity provider** — supplies metadata and an SSO endpoint. It is not
  a live sign-in participant in V1.
- **Tenant member** — starts an OIDC login after an operator has saved a
  runtime-complete connection.

## Pre-conditions

- The operator is authenticated with one of the two SSO-admin scopes. The BFF
  does not authorize writes from a tenant-admin session.
- The connection input has a valid tenant id, protocol, IdP entity/issuer, at
  least one HTTP(S) endpoint, positive session and refresh lifetimes, and a
  required email mapping.
- A live OIDC connection also has `oidcClientId`, `oidcJwksUrl`, and an `sso`
  authorization endpoint.
- The tenant-admin reader has a session whose payload contains the tenant id
  used by `GET /v1/tenant-console/sso`.
- Deployable BFF boot requires the admin-database snapshot sink and hydrates it
  before traffic. Missing or failed primary persistence fails closed; a route
  cannot acknowledge a connection mutation that exists only in process memory.

## Steps

### 1. Tenant admin inspects the read-only identity cockpit

- [x] `/identity` redirects an unauthenticated visitor before any tenant state
      renders.
- [x] The page reads `GET /v1/tenant-console/sso` with the session tenant id and
      renders connection id, protocol, IdP entity, allowed flows, JIT state,
      session minutes, and mapping count.
- [x] Empty and backend-unavailable states are explicit.
- [x] A second section runs SSO, SCIM, roster, auth-challenge, and federation
      engines against labeled sample inputs.
- [ ] The page has no create, import, edit, test, delete, or policy-write
      control. Its sample `JIT ready` and `step-up-required` values are
      demonstrations, not the saved tenant's evaluated policy.

### 2. Platform operator creates or imports a connection

The write surface is `/operator/sso`.

**Manual create**

- [x] `POST /v1/admin/sso` accepts an already-shaped `SsoConnectionInput`.
- [x] Store validation rejects an invalid protocol, missing endpoint, bad URL,
      non-positive lifetime, or a mapping set without a required email slot.

**SAML metadata import**

- [x] `POST /v1/admin/sso/import` extracts entity id, SSO/SLO endpoints, and
      certificate thumbprint from XML.
- [x] Missing entity id, SSO endpoint, or certificate returns structured
      validation issues and writes nothing.

**OIDC discovery import**

- [x] The import route fetches discovery and persists issuer, authorization
      endpoint, JWKS URL, and client id.
- [x] Closed-port, 404, 503, and missing-`jwks_uri` cases fail closed and leave
      the roster unchanged.

### 3. Operator edits mappings, flows, and lifetimes

- [x] The mapping editor exposes exactly: `email`, `displayName`, `familyName`,
      `givenName`, `groups`, `tenant-role`, and `preferred-locale`.
- [x] Each row stores an external claim, required flag, and one transform:
      `identity`, `lowercase`, `csv-split`, or `first-only`.
- [x] The editor also persists IdP/SP flow flags, JIT, session lifetime, and
      refresh lifetime through `PATCH /v1/admin/sso/:id`.
- [x] Config writes use a serialized candidate/save/publish queue through
      `wireDurableSsoConnections`; create, import, update, and delete
      acknowledge only after the primary snapshot commits.
- [ ] `groups` and `tenant-role` remain mapped attributes only. No route
      translates group strings into tenant role templates.
- [ ] There is no draft/probed/enabled status, reviewer gate, or activation
      timestamp.

### 4. Operator runs the connection probe

- [x] **Test** calls `POST /v1/admin/sso/:id/test` and renders a structured,
      fail-closed verdict.
- [x] OIDC checks discovery plus a JWKS containing at least one signing key.
- [x] SAML checks endpoint liveness and certificate-thumbprint shape.
- [x] A failed check returns HTTP 200 with `ok: false` so the UI can name the
      failed probe without fabricating reachability.
- [ ] This is a connection probe, not a sandbox user login. It does not post an
      assertion, apply mappings, provision an account, or exercise MFA.

### 5. Tenant admin reads the fixed auth-policy demonstration

- [x] `evaluateAuthChallenge` supports MFA, IPv4 allowlists, device posture,
      inactivity, and named step-up triggers.
- [x] The tenant-admin page's fixed sample intentionally returns
      `step-up-required / device-posture`.
- [ ] No persisted `TenantAuthPolicy` is loaded for the tenant.
- [ ] No UI or API writes MFA methods, triggers, CIDRs, posture requirements,
      refresh interval, or inactivity interval.
- [ ] The CIDR helper is IPv4-only; IPv6 never matches an allowlist entry.

### 6. Member completes the live OIDC login

- [x] `GET /v1/auth/sso/:connectionId/login` requires an OIDC connection with SP
      initiation enabled and runtime fields present.
- [x] It issues single-use state and nonce in a process-local map with a
      ten-minute TTL, then redirects to the IdP using `response_type=id_token`
      and `response_mode=form_post`.
- [x] `POST /v1/auth/sso/callback` consumes state before verification, fetches
      JWKS, verifies signature, issuer, audience, nonce, expiry, and algorithm,
      then applies the saved claim mappings.
- [x] The callback finds an existing user by mapped email or calls
      `provisionFederatedUser` when JIT is allowed, then returns a real customer
      session.
- [x] Missing required claims, disabled JIT, disabled SP flow, unknown state,
      JWKS failure, and signature failure never mint a session.
- [ ] IdP-initiated login is modeled by the domain function but has no live
      route. SAML login returns `503 sso_protocol_not_supported`.
- [ ] The process-local state/nonce map is not shared across BFF replicas.

### 7. Operator audits, updates, or removes the connection

- [x] Successful create/import/update/delete/probe calls append
      `tenant.sso.connection.*` or `tenant.sso.probe.completed` events with
      tenant, connection, protocol, and IdP context.
- [x] Duplicate deletion returns 404 and does not mint another delete event.
- [x] Tenant-admin read-back reflects later updates and deletion through the
      tenant-filtered read route.
- [ ] Login success/failure does not join the admin SSO audit chain in this
      route module.

## Post-conditions

- Every accepted create, import, update, and delete is committed to the primary
  snapshot before the response. A failed save returns `503` without publishing
  volatile state, and restart restores the canonical committed roster.
- The operator roster and tenant-filtered read-only roster agree on the saved
  connection.
- OIDC can mint a customer session through the live callback; SAML cannot.
- The probe and login have distinct evidence: reachability does not imply a
  successful user assertion.
- No statement in this journey treats sample auth-policy cards, claim mapping,
  or connection persistence as an enablement state or role-template binding.

## Failure modes

- **Wrong tenant visibility** — the tenant-console read is filtered by session
  tenant; the operator admin endpoint can list all tenants or an explicit filter
  and therefore requires privileged scope.
- **Invalid import** — structured issues are returned and no connection is
  created.
- **Persistence unavailable** — mutation returns stable `503`; the prior
  committed roster remains live and is the state restored after restart.
- **Probe failure** — the connection remains saved; `ok: false` is evidence to
  repair configuration, not an automatic disable transition.
- **Required claim missing** — callback returns a named mapping error before
  provisioning.
- **Unknown or expired state** — callback returns 400; state is single-use even
  when later verification fails.
- **Flow toggled off after login starts** — callback mapping fails closed with
  `flow-not-permitted`.
- **OIDC key mismatch** — verification returns 401 and no session.
- **SAML member attempts login** — V1 returns 503 because only the configuration
  and probe paths exist.
- **IPv6 allowlist input** — the current evaluator returns `ip-blocked` when an
  IPv4-only CIDR cannot match it.
- **Replica switch during callback** — process-local state may be absent on the
  receiving replica.

## E2E coverage

- [`apps/oshun/web/e2e/tenant-sso-config.spec.ts`](../../apps/oshun/web/e2e/tenant-sso-config.spec.ts)
  drives the operator roster, manual create, both imports, all mapping slots and
  transforms, edit/delete, and successful plus failed Test verdicts.
- [`apps/oshun/web/e2e/tenant-sso-crud-bff.spec.ts`](../../apps/oshun/web/e2e/tenant-sso-crud-bff.spec.ts)
  drives CRUD, validation, import failure atomicity, probe outcomes, auth gates,
  and the admin audit chain over real HTTP. Its eight cases passed against an
  isolated current-source BFF, fresh migrated PostgreSQL database, and durable
  Redis at source commit `257b390ac439866669a9952bde1c2be57d7f4975`.
- [`apps/oshun/web/e2e/tenant-sso-oidc-login-bff.spec.ts`](../../apps/oshun/web/e2e/tenant-sso-oidc-login-bff.spec.ts)
  drives signed-token OIDC login, transformations, JIT/existing-user behavior,
  state and flow failures, missing claims, and untrusted keys.
- [`apps/oshun/tenant-admin/e2e/tenant-identity.spec.ts`](../../apps/oshun/tenant-admin/e2e/tenant-identity.spec.ts)
  drives session gating, tenant-isolated roster read-back, sample policy
  evidence, empty/error states, and accessibility.
- [`apps/oshun/web/e2e/admin-auth-policy-bff.spec.ts`](../../apps/oshun/web/e2e/admin-auth-policy-bff.spec.ts)
  covers a separate studio evaluator. It is not evidence of a tenant auth-policy
  editor.

Coverage is **partial**: the two real operator/OIDC spines are deep, but the
tenant-admin setup editor, tenant auth-policy persistence, activation state,
role binding, IdP-initiated route, and SAML login do not exist.

## Per-view files touched by this journey

- [`TENANT_ADMIN_WALKTHROUGH/identity.md`](../../TENANT_ADMIN_WALKTHROUGH/identity.md)
  — tenant-scoped read-only roster and fixed engine samples.
- [`operator/operator-sso.md`](../operator/operator-sso.md) — privileged
  connection editor and probe.
- [`operator/operator-tenant.md`](../operator/operator-tenant.md) — membership
  console; it does not embed SSO setup.
- [`shell/04-auth-session.md`](../shell/04-auth-session.md) — customer session
  boundary after OIDC callback.
- [`operator/operator-audit.md`](../operator/operator-audit.md) — searchable SSO
  management events.

## Cross-references

- [`tenant-bulk-member-invite-scim.md`](./tenant-bulk-member-invite-scim.md) —
  separate invite and SCIM projections; neither enables this connection.
- [`tenant-onboard.md`](./tenant-onboard.md) — invite acceptance; it does not
  automatically start SSO.
- [`tenant-audit-log-investigation.md`](./tenant-audit-log-investigation.md) —
  management-event inspection.
- `libs/oshun/tenant-console/src/identity/sso.ts` — mapping and login domain
  contract.
- `libs/oshun/tenant-console/src/identity/federation-and-auth-policy.ts` — pure
  policy evaluators.
- `apps/oshun/bff/src/tenant-console/sso-route.ts` — privileged management and
  probe routes.
- `apps/oshun/bff/src/auth/sso-login-routes.ts` — live OIDC-only runtime.

## Open questions

- [ ] Which tenant-admin permission, if any, should be allowed to mutate its own
      connections without the platform operator's global route?
- [ ] Should connections gain an explicit `draft → probed → enabled → disabled`
      state?
- [ ] Where should a persisted per-tenant auth policy live, and which runtime
      requests must enforce it?
- [ ] How do mapped `groups` and `tenant-role` values bind to approved tenant
      roles?
- [ ] Should login state move to a shared, single-use store for multi-replica
      deployments?
- [ ] What is the supported IdP-initiated URL contract?
- [ ] Is SAML login in V1 scope, or should the editor label it
      configuration/probe only?
- [ ] What is the explicit IPv6 allowlist policy?
