---
path: /operator/sso
surface: operator
domain: operator (tenant — SSO/SAML/OIDC config)
auth: signed-in
source: apps/oshun/web/src/app/operator/sso/page.tsx
status: walked
last_walked:
  '2026-06-25 operator SSO roster row-cell addendum — direct signed-in
  /operator/sso render, generatedAt anchor, live roster/masthead count
  attributes, seeded SAML/OIDC card metadata attributes, visible
  tenant/protocol/IdP/sync field cells, footer targets, anonymous
  redirect-before-render, editor create/import/edit/delete/test legs, and real
  BFF read/write/import/probe contracts verified in tenant-sso-config.spec.ts.
  Evidence: WALKTHROUGH/results/v1-real-infra-run-2026-06-22.md §91.'
---

# Operator · SSO connections

## Purpose

The tenant federation roster: one card per tenant SSO connection from
`GET /v1/sso`, showing protocol (SAML/OIDC), status, the IdP metadata pointer,
and last sync time. The roster is followed by the shipped `OperatorSsoEditor`,
which manages SSO connections through the live admin SSO BFF: create, import
metadata/discovery, edit claim mappings, delete, and run the connection test.

## Entry points

- **From `/operator`** (hub) — operator-surface card; foot "← Operator hub"
  returns there
- **Cross-links from siblings** — `/operator/audit` foot row links here;
  `/operator/tenants` links here; this page links onward to `/operator/tenants`
  `/operator/lms`, and `/operator/audit`
- **From tenant onboarding** — after onboarding a tenant, configuring SSO
  surfaces here (the empty state says "Onboard a tenant to start")
- **Direct URL** — session-gated only (`proxy.ts` redirects sessionless →
  `/welcome?redirect=`); no operator-role gate in the proxy
- **Bookmark** — `canonical = '/operator/sso'`

## Layout regions

`page.tsx` is an async server component: `await bffGet<SsoResponse>('/v1/sso')`
(falling back to `{ connections: [] }`), rendered inside
`<LWebShell top={<LCustomerNav active="library" />}>` (reuses the **customer**
nav, Library active). Centered column `maxWidth: 1080`.

- **Header**: `LCustomerNav` (Library highlighted — verify intended chrome)
- **Page anchor**: outer content column exposes `data-operator-sso-page` and
  `data-operator-sso-generated-at`
- **Masthead**: `LMasthead` wrapped by `data-operator-sso-masthead` with
  `data-operator-sso-connection-count`; visible copy is left "Operator · SSO",
  right `${data.connections.length} connections`, kicker "Tenant federation",
  title "The _SSO_ roster."
- **Roster**: `<div data-operator-sso-roster data-operator-sso-roster-count>`
  with a grid of connection cards OR the empty card
- **Connection card** (per `row`, `<div data-operator-sso-card>`): accent border
  when `status === 'active'`; stable attributes expose `data-operator-sso-id`,
  `data-operator-sso-tenant-id`, `data-operator-sso-tenant-label`,
  `data-operator-sso-protocol`, `data-operator-sso-status`,
  `data-operator-sso-idp-metadata-url`, and `data-operator-sso-last-sync-iso`
  - Top line: "Tenant · {tenantId}" eyebrow + `tenantLabel` (italic when
    active), right-aligned "{PROTOCOL} · {status}" (accent when active)
  - "IdP metadata · {idpMetadataUrl}" (mono, `word-break: break-all`)
  - "Last sync · {localized lastSyncIso | 'never'}" (serif italic)
  - Visible field cells expose
    `data-operator-sso-field="tenant-eyebrow" | "tenant-label" | "protocol-status" | "idp-metadata" | "last-sync"`
- **Editor**: `<OperatorSsoEditor data-sso-editor>` — lists live admin SSO
  connections, exposes Test / Edit / Delete on each row, renders an Import
  metadata form for SAML XML or OIDC discovery, and renders a New/Edit
  connection form for tenant id, protocol, IdP entity/issuer, thumbprint,
  endpoints, claim mappings, flow toggles, JIT, and session/refresh lifetimes
- **Footer action row**: `LBtn` ghost "← Operator hub" (`/operator`), `LBtn`
  ghost "Tenants" (`/operator/tenants`), `LBtn` ghost "LMS" (`/operator/lms`),
  `LBtn` primary "Audit log" (`/operator/audit`)

## States

- [x] **Loading** — source-verified 2026-06-25: no route-level `loading.tsx`;
      server-side `await bffGet` blocks the roster render, so there is no
      skeleton state. The client editor separately renders `[data-sso-loading]`
      while its admin list request is pending
- [x] **Anonymous user** — Playwright 2026-06-25: sessionless `/operator/sso`
      redirects to `/welcome?...` before `[data-operator-sso-page]` or
      `[data-operator-sso-roster]` render
- [x] **Signed-in user** — Playwright 2026-06-25: primed signed-in session plus
      operator admin bearer renders the page, generatedAt anchor, masthead/
      roster count attributes, live roster cards, footer, and editor. Still
      session-gated only; no operator-role gate in the proxy or in-page guard
- [x] **Empty** — unit-covered: `src/app/operator/sso/__tests__/page.test.tsx`
      renders `data.connections.length === 0` and asserts "No SSO connections
      configured. Onboard a tenant to start." plus "0 connections"
- [x] **Populated** — Playwright: real admin BFF seeds live SAML/OIDC
      connections and `/operator/sso` renders one card per connection; active
      cards expose accent status, stable protocol/status/id/tenant/metadata/sync
      attributes, and visible field cells
- [x] **Protocol diversity** — Playwright asserts both "SAML · active" and "OIDC
      · active" from live seeded connections
- [ ] **Status diversity** — active is browser-covered; pending / disabled /
      error remain BFF-envelope/source-visible only and need dedicated seeded
      browser cases if the store can create those states
- [x] **`lastSyncIso` null** — Playwright asserts "Last sync · never"
- [x] **BFF unreachable / non-2xx** — unit-covered:
      `src/app/operator/sso/__tests__/page.test.tsx` mocks `bffGet` returning
      `null`; the page substitutes `{ connections: [] }` and shows the empty
      state (no fabricated connections). Dedicated outage-route E2E is not
      forceable through `page.route` because this is a server component
- [ ] **Offline** — RSC fetch (`revalidate 60`); operator SW policy TBD
- [ ] **Standalone PWA** — renders; verify robots/no-index (no `robots` export
      here)

## Interactions

### Masthead

- [x] **Masthead labels** — non-interactive; Playwright asserts "Operator ·
      SSO", "Tenant federation", the exact live "`N` connections" text, and
      matching masthead/roster count attributes

### Connection card (per `row` in `data.connections`)

- [x] **Tenant eyebrow + label** (text) — "Tenant · {tenantId}" + `tenantLabel`;
      non-interactive; Playwright asserts seeded SAML/OIDC attributes and
      visible field cells (no click into a tenant or connection-edit view)
- [x] **Protocol · status** (text) — "{PROTOCOL} · {status}"; non-interactive
- [x] **IdP metadata URL** (text) — displayed verbatim; **not a link** (plain
      text with `word-break: break-all`); verify whether it should be clickable
      to the IdP metadata document. Playwright pins SAML metadata URL verbatim
      and OIDC issuer-derived discovery URL
- [x] **Last sync** (text) — localized or "never"; non-interactive
- [x] **Card** — static roster card is non-interactive; edit/test/delete live in
      the editor row below, keyed by admin connection id

### SSO editor

- [x] **List** — loads the live admin SSO list through `ssoClient.list`; browser
      E2E bridges the request to the real BFF with an operator-admin bearer
- [x] **Create connection** — Playwright 2026-06-25 fills the New connection
      form for a valid OIDC connection, submits, verifies the form reset, reads
      the persisted connection back from the BFF, and sees the new editor row
- [x] **Import metadata** — Playwright 2026-06-26 drives the Import metadata
      form for SAML XML and OIDC discovery through the live
      `POST /v1/admin/sso/import` route, reloads the editor roster, and verifies
      parsed entity/issuer, endpoint, thumbprint/JWKS, and required email claim
      mapping persistence by BFF read-back
- [x] **Edit connection** — Playwright opens a prefilled SAML connection,
      changes the required email mapping external claim, saves, and verifies the
      persisted BFF store
- [x] **Claim-mapping matrix** — Playwright adds/persists all seven internal
      attribute slots and all four transform options through the browser editor
      and BFF read-back
- [x] **Delete connection** — Playwright deletes a connection and verifies the
      editor row disappears and the BFF store no longer lists it
- [x] **Test connection** — Playwright runs the browser Test action against real
      local OIDC discovery/JWKS endpoints and asserts both the reachable verdict
      and the fail-closed `oidc-jwks: fail` verdict
- [x] **Client validation** — unit-covered: `OperatorSsoEditor.test.tsx` asserts
      the required email mapping and BFF validation-error surfaces

### Footer actions

- [x] **"← Operator hub"** (`LBtn` ghost, link) → `/operator`
- [x] **"Tenants"** (`LBtn` ghost, link) → `/operator/tenants`
- [x] **"LMS"** (`LBtn` ghost, link) → `/operator/lms`
- [x] **"Audit log"** (`LBtn` primary, link) → `/operator/audit`

## Data & contracts

- **Reads**: `bffGet<SsoResponse>('/v1/sso')` —
  `{ generatedAt: string; connections: Connection[] }` where
  `Connection = { id, tenantId, tenantLabel, protocol: 'saml'|'oidc', status: 'active'|'pending'|'disabled', idpMetadataUrl, lastSyncIso: string | null }`.
  On `null` substitutes an empty list
- **Writes**: `OperatorSsoEditor` uses `/v1/admin/sso` for create/list/update/
  delete, `/v1/admin/sso/import` for metadata/discovery import, and
  `/v1/admin/sso/:id/test` for probe results
- **Realtime**: _None observed._
- **Caching**: RSC fetch, `next.revalidate = 60`, 5 s abort
- **Auth/role check**: session-gated only (`proxy.ts` redirects sessionless →
  `/welcome?redirect=`); no operator-role gate in the proxy and no in-page guard

## Cross-references

- Parent: [`operator-overview.md`](./operator-overview.md)
- Sibling operator routes:
  - [`operator-tenants.md`](./operator-tenants.md)
  - [`operator-audit.md`](./operator-audit.md)
  - [`operator-incidents.md`](./operator-incidents.md)
  - [`operator-tenant.md`](./operator-tenant.md) — per-tenant member console
  - [`operator-lms.md`](./operator-lms.md)
- Feature spec: [`V1/features.md`](../../V1/features.md#product-surfaces)
- Architecture:
  [`V1/ARCHITECTURE.md`](../../V1/ARCHITECTURE.md#operator-surfaces)
- Journeys traversing this view:
  - [`../journeys/tenant-sso-config-saml-oidc-claim-mapping.md`](../journeys/tenant-sso-config-saml-oidc-claim-mapping.md)
  - [`../journeys/tenant-onboard.md`](../journeys/tenant-onboard.md)

## E2E coverage

- [`apps/oshun/web/e2e/tenant-sso-config.spec.ts`](../../apps/oshun/web/e2e/tenant-sso-config.spec.ts)
  — direct `/operator/sso` signed-in render, stable page/masthead/roster/card/
  field/footer anchors, generatedAt ISO attribute, live card count tied to the
  masthead and roster attributes, live BFF-seeded SAML and OIDC roster cards,
  seeded card id/tenant/label/protocol/status/metadata/sync attributes, visible
  tenant/protocol/IdP/sync field cells, footer targets (`/operator`,
  `/operator/tenants`, `/operator/lms`, `/operator/audit`), anonymous
  redirect-before-render, `/v1/sso` real-HTTP envelope tripwire, and browser
  editor create/import/edit/delete/test flows over the live admin SSO BFF via
  the operator-admin bridge.
- [`apps/oshun/web/e2e/tenant-sso-crud-bff.spec.ts`](../../apps/oshun/web/e2e/tenant-sso-crud-bff.spec.ts)
  — pure-BFF create/list/update/test/delete, validation, metadata import, and
  successful/fail-closed probe contracts.
- [`apps/oshun/web/e2e/tenant-sso-oidc-login-bff.spec.ts`](../../apps/oshun/web/e2e/tenant-sso-oidc-login-bff.spec.ts)
  — real OIDC login/callback runtime with signed local id token, claim
  transforms, JIT, and failure verdicts.
- Unit coverage: `apps/oshun/web/src/app/operator/sso/__tests__/page.test.tsx`
  covers server-component empty and BFF-null fallback branches;
  `apps/oshun/web/src/components/operator/OperatorSsoEditor.test.tsx` covers
  client validation and API-error rendering.

## Open questions / known gaps

- [x] The operator page now has a live SSO editor for create/edit/delete/test
      plus metadata import and claim mappings; this older read-only roster gap
      is closed on the operator surface. The separate tenant-admin metadata
      upload/discovery and enable-state-machine gaps remain journey-level
      product gaps
- [ ] `idpMetadataUrl` is plain text, not a link — confirm whether operators
      should be able to open the IdP metadata document
- [ ] No way to trigger a sync from this view; `lastSyncIso` is display-only —
      document the sync trigger path
- [ ] Customer `LCustomerNav` (Library active) chrome on an operator route and
      no `robots: { index: false }` — confirm intended
- [ ] An operator-role gate appears absent — `proxy.ts` only redirects
      sessionless users to `/welcome`; no role check in the proxy or page
      component (possible gap vs intended scope)
