Blocks

SSO Setup

A four-state SAML/OIDC connection panel — copyable values in both directions, DNS domain proof, claim mapping, a test verdict that names the broken setting, and an enforcement switch that spells out who gets locked out.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import {
  ArrowRight,
  Check,
  CircleAlert,
  CircleCheck,
  Copy,
  ExternalLink,
  Globe,
  KeyRound,
  LoaderCircle,
  RefreshCw,

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/sso-setup.json

Prompt

Build a React + TypeScript + Tailwind "SsoSetup" block (lucide-react icons, zod,
a clipboard hook and a confirmation dialog).

Contract
- One zod schema is the single source of truth; props are z.infer of it plus
  optional callbacks and the native <section> props.
  { status: "loading" | "empty" | "error" | "ready";
    asOf: ISO 8601 instant — the block's ONLY clock;
    provider: { name, protocol: "saml" | "oidc", docsUrl? } | null;
    serviceValues: { acsUrl, entityId, metadataUrl } | null;      // ours to hand over
    idpValues: { issuer, ssoUrl, metadataUrl, certificateFingerprint,
                 certificateExpiresAt } | null;                    // theirs to hand back,
                                                                   // every field nullable
    domains: { domain, verified, txtRecord, recordName?, lastCheckedAt? }[];
    mappings: { claim, attribute, required?, sample? }[];
    test?: { status: "passed" | "failed" | "never_run", message, checkedAt,
             code?, details? } | null;
    enforcement: { enabled, memberCount, exemptCount, strandedCount,
                   strandedSample? };
    errorMessage? }
- Refinements worth keeping: ready requires provider + serviceValues + idpValues;
  a finished test has a checkedAt; exemptCount <= memberCount and
  strandedCount <= memberCount - exemptCount, so no sentence can add up wrong.
- Callbacks: onCheckDomain(domain), onRunTest(), onEnforcementChange(enabled),
  onConnect() for the empty branch, onRetry() for the error branch. Each one
  gates its own affordance — no handler, no button. Returning a promise drives
  that control's pending state; the verdict arrives as the next payload, never
  as a return value.

Behavior
- Four first-class branches: loading (skeletons shaped like the real value rows
  plus the policy row, sr-only status line), empty (no connection exists, so no
  per-connection values exist either — say that instead of rendering four empty
  steps), error (the payload's errorMessage, else a neutral "nothing was
  changed", plus retry), ready (four numbered steps, a test panel and the policy
  footer).
- Step 1, ours to hand over: every value gets its own copy button, the code stays
  select-all, and a rejected clipboard write selects the text and says to press
  Ctrl/Cmd+C — the one path that must not end in silence for a 32-pair SHA-256
  fingerprint nobody is going to retype.
- Step 2, theirs to hand back: a null field renders a dashed "Not provided yet"
  row with the note explaining what it is for, never a blank. The signing
  certificate's expiry renders as a date plus the days left, and once it is past
  asOf it turns destructive and says sign-in is failing.
- Step 3, domain proof: unverified rows show the whole TXT record (host and
  value, each copyable) plus the last lookup time or "Never checked"; verified
  rows collapse to a badge. The header counts "N of M verified" from the array.
- Step 4, claim mapping: claim on the left, our field on the right, required
  marked. An empty claim renders "Not mapped" — destructive when required, muted
  when not — and a `sample` from the last passing test is shown as evidence. An
  empty array states the rule ("until a claim is mapped to each required field,
  sign-in cannot complete") rather than promising that a test will discover the
  claims for you: what a test populates is the host's business.
- Test panel: passed / failed / never run, the payload's own sentence, the
  evidence lines verbatim, and a fix hint keyed on protocol AND failure code
  (clock_skew, unsigned_assertion, missing_claim, audience_mismatch,
  certificate_mismatch, other) — the SAML audience hint names the SP entity ID,
  the OIDC one the Client ID, so the hint always names a label that is on the
  screen. Hints carry no numbers and no step ordinals: measured deltas and
  tolerances belong to the payload, and the step order is a layout choice.
- Enforcement: every prerequisite is DERIVED at render — IdP values complete
  (issuer plus either metadata or endpoint-and-certificate), certificate not
  expired, at least one verified domain, every required claim mapped, last test
  passed. A stored "ready" flag drifts from the values on screen and the drift
  is discovered by whoever gets locked out.
- A blocked switch is never a dead disabled button: the unmet prerequisites are
  listed on screen, the switch keeps aria-disabled (not the native attribute) so
  it stays focusable, and pressing it moves focus to that list and says how many
  items are left. Turning enforcement OFF is never blocked. Each blocker names
  only the half that is actually missing — the IdP one asks for the issuer, or
  for the key material, or for both, never for a value already rendered above.
- Enabling goes through a confirmation that states the blast radius from the
  data — how many of how many members move, how many exempt members keep
  passwords, and how many would be stranded on an unverified domain, with sample
  addresses. When anyone would be stranded the dialog escalates to
  type-to-confirm and destructive styling; the phrase is one lowercase token with
  no space, because the comparison is verbatim and a touch keyboard capitalising
  a sentence would leave the confirm button dead with nothing saying why.

Rendering & styling
- Semantic tokens only: bg-card / border / bg-muted for surfaces, primary for the
  live switch, destructive for failures and lockouts, var(--chart-2) for the
  verified and passed marks. No hex, no palette classes.
- Values are font-mono text-xs with break-all so a long ACS URL never widens the
  card; the block is legible from ~420px with no horizontal scroll.
- Time comes from asOf through Intl.DateTimeFormat (timeZone UTC) and
  RelativeTimeFormat, so the server and the browser render the same string.
- Spinners carry motion-reduce:animate-none, the knob transition is disabled
  under reduced motion, and every button has a focus-visible ring. cn() merges
  className; remaining props spread onto the root section.

Customization levers
- Field labels: SERVICE_FIELDS, IDP_FIELDS and FAILURE_HINT are constant maps
  keyed by protocol — retitle every row for your own product (or add a third
  protocol) without touching layout or logic. Retitle the entity ID row and the
  audience hint together: the hint exists to name the label that is on screen.
- Prerequisites: the blocker list is one array built at the top of the component.
  Add "SCIM provisioning connected" or drop the test requirement by editing that
  block; the switch, the count and the focus behaviour follow automatically.
- Sections: each step is an independent <Step> child. Remove step 3 for a product
  that federates without domain claims, or reorder them; the numbering is a prop.
- Confirmation strength: the type-to-confirm phrase is applied when
  strandedCount > 0 — make it unconditional for a stricter product, or drop it.
- Density: swap p-4 for p-3 and gap-3 for gap-2 across the steps for an
  admin-console-density variant; the copy rows keep their hit area.
- Accent: var(--chart-2) marks health. Point it at another chart token, or at
  primary, to match a palette where green means something else.

Concepts

  • Two-way exchange — the screen is not a form; it is a handover between two consoles. Half the values are ours to give (each with a copy button, because they are pasted elsewhere) and half are theirs to bring back (each with a visible gap until they do).
  • Domain proof before trust — a TXT record is what stops one tenant from federating an email domain it does not own, so the unverified row shows the host and the value in full and reports when DNS was last checked, rather than hiding the record behind "pending".
  • Derived readiness — whether SSO can be required is recomputed from the payload every render: IdP values complete, certificate in date, a verified domain, required claims mapped, last test passed. A stored flag would drift from the values on screen, and the drift is only discovered by the company that cannot sign in.
  • Blocked with reasons — the switch that cannot be thrown keeps aria-disabled instead of the native attribute, so it stays focusable; the unmet prerequisites are on screen, and pressing it moves focus to them. A dead disabled control is the defect this panel exists to avoid.
  • Failure that names the setting — the test verdict carries a code, so "SSO failed" becomes clock skew, an unsigned assertion, or a missing email claim, with the measured evidence from the payload and a fix that points at one setting.
  • Blast radius before the confirm — enabling enforcement states how many of how many members move, who stays exempt, and who is stranded on an unverified domain — by address — and escalates to type-to-confirm exactly when someone would lose access entirely.

On This Page