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.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/sso-setup.jsonPrompt
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-disabledinstead 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.
Data Export Request
A GDPR-style data export block — pick scopes against live size estimates, confirm a request that states what the archive contains and who can open it, watch a build that never dresses a queue position up as progress, and download a link that visibly counts down to its own expiry.
Trust Center
The public security page a B2B buyer asks for — audit dates measured against each report's own refresh cycle, a filterable subprocessor table with DPA links, plain-language practices, uptime, and when a human last reviewed the page.