SSO β Status & Path to Offering It to Customers
Where per-tenant SSO stands today, and exactly what has to happen before we can sell and onboard a customer. Companion to:
sso-uth-runbook.mdβ turnkey per-customer onboarding checklistsso-implementation.mdβ architecture + original design- Epic #2589 and phase issues #2586 (Phase 1) / #2587 (Phase 2) / #2588 (Phase 3)
TL;DR
The SAML 2.0 Single Sign-On feature is built, tested, and merged to main
(commit 18c2d0d76, security-hardened in a follow-up). The production database
tables are already applied. This is not a development project β it is a
small platform fix plus configuration, onboarding tooling, and documentation.
You could onboard a friendly SAML customer this week. The only gating code change is pinning one environment variable (PR #2585); the only gating operational change is setting an absolute session timeout in Supabase.
What already exists (feature-complete)
| Piece | Location | State |
|---|---|---|
| SAML crypto (verify, metadata, attribute mapping) | workers/api/src/services/saml.ts | Built (@node-saml/node-saml) |
| SP endpoints β metadata / login / ACS | workers/api/src/routes/sso.ts | Built, Lambda-only mount |
| JIT provisioning + cross-tenant / replay guards | workers/api/src/services/sso-provisioning.ts | Built |
| Tenant-admin config API | workers/api/src/routes/tenant-sso-admin.ts | Built |
| Tenant-admin config UI | apps/web/src/app/tenant-admin/sso/ | Built |
| User SSO start + session callback pages | apps/web/src/app/auth/sso/ | Built |
DB tables tenant_sso_config, tenant_sso_logins | migration 254_tenant_sso.sql | Applied in prod |
| Tests | workers/api/src/__tests__/**/*sso*, *saml* | 5 suites, comprehensive |
Architecture in one line: we are our own per-tenant SAML SP (not Supabase
native SSO). The customerβs IdP signs an assertion β our ACS endpoint verifies
the signature against their stored X.509 cert, checks the audience, enforces a
verified-email-domain allowlist, JIT-provisions the user into the right tenant,
guards against replay, mints a Supabase session, and hands tokens to the frontend
in a URL fragment. Runs on the Lambda API (api-pdf.gevell.com) and serves
pdf.gevell.com.
What still has to happen
Phase 1 β Platform readiness (do once) β issue #2586
- Pin
PUBLIC_BASE_URLon the API Lambda β PR #2585 (this branch). The SP derives its entity ID + ACS URL from this var and falls back to the request origin when unset, which silently breaks the audience check. Interim manual fix:Terminal window aws lambda update-function-configuration \--function-name accessible-pdf-prod-api \--environment "Variables={PUBLIC_BASE_URL=https://api-pdf.gevell.com}" - Set an absolute session timeout (Supabase Dashboard β Authentication β
Sessions). Today every session has
not_after = NULLβ sessions never expire, one was observed refreshing for 56 days. Combined with no SLO and no SCIM, an IdP-revoked user stays signed in indefinitely. This is the #1 thing an enterprise/university security review will flag.
Phase 2 β Customer onboarding flow (repeatable) β issue #2587
- Generalize the UTHealth runbook to any customer.
- Build the intake web page (see below) to collect IdP details.
- Write customer-facing docs (see below).
Phase 3 β Enterprise-readiness backlog (as deals require) β issue #2588
SLO + session revocation Β· SCIM deprovisioning Β· SSO enforcement (sso_enforce) Β·
OIDC Β· cross-app SSO (audit/acr/orgchart) Β· SP request signing / encrypted
assertions. All documented as v1 gaps in the runbook Β§9.
What information we need from the customer
This is the intake checklist β everything required to configure a tenant. Their IAM/IT team supplies most of it.
| # | Field | Why we need it | Example |
|---|---|---|---|
| 1 | Organization name + desired slug | Slug is permanent; appears in every SSO URL | UTHealth Houston / uth |
| 2 | IdP stack | Attribute names differ substantially | Microsoft Entra ID, Shibboleth/InCommon, Okta, Google |
| 3 | IdP metadata URL or XML | Source of entity ID, SSO URL, signing cert | https://idp.uth.edu/.../metadata |
| 4 | Released email attribute name | Exact attribute the assertion carries the email in | urn:oid:0.9.2342.19200300.100.1.3 (Shib) or .../emailaddress (Entra) |
| 5 | Actual released mail domain(s) | Feeds the anti-takeover allowlist β must match reality | uth.edu, uth.tmc.edu |
| 6 | Display-name attribute (recommended) | Populates the user profile | urn:oid:2.16.840.1.113730.3.1.241 |
| 7 | Department/affiliation attribute + groupβdepartment map (optional) | Only if they want users foldered by unit | eduPersonAffiliation β dept slug |
| 8 | NameID format | Usually emailAddress | nameid-format:emailAddress |
| 9 | Technical contact (IAM) | Metadata exchange + cert-rotation notifications | name/email |
| 10 | Signing-cert expiry | Cert rotation is the #1 cause of dead SSO later | notAfter date |
| 11 | Confirmation they can accept our v1 limitations | No SLO/SCIM/enforcement in v1 | signed-off by their security review |
What we give them in exchange (the SP coordinates):
- SP Entity ID / metadata URL:
https://api-pdf.gevell.com/api/sso/<slug>/metadata - ACS / Reply URL:
https://api-pdf.gevell.com/api/sso/<slug>/acs - Binding: HTTP-POST Β· NameID: emailAddress Β· Signed AuthnRequests: no Β· Encrypted assertions: no (v1)
User documentation we still need to write
Two distinct audiences:
- IdP-administrator setup guide (their IT team) β the most important new doc.
- The SP coordinates to register (entity ID, ACS URL, metadata URL).
- Which attributes to release (email mandatory, name recommended, dept optional).
- Step-by-step screenshots for the two common stacks: Microsoft Entra ID (Enterprise Application β SAML) and Shibboleth/InCommon.
- Our v1 limitations, stated plainly (no SLO, no SCIM, no encrypted assertions).
- Cert-rotation expectations (βtell us 60 days before you roll the signing certβ).
- End-user βSign in with SSOβ help page β short. βGo to pdf.gevell.com/auth/sso/start, type your organization, sign in with your normal institutional login.β Where to go if it fails (contact your IT).
- Tenant-admin self-service guide β how an org admin uses
/tenant-admin/ssoto paste metadata, set the domain allowlist, test, and enable. (The UI exists; it needs a short walkthrough doc.)
These live under docs/user/ (Docusaurus) for the customer-facing ones; the
tenant-admin guide can start in docs/admin/.
Can we add a web page that gathers the setup info?
Yes β recommended, and itβs Phase 2 work (#2587). Two options:
- Option A β extend the existing tenant-admin UI (
/tenant-admin/sso). It already has a βpaste IdP metadata XML β parseβ flow that auto-extracts the entity ID, SSO URL, and signing cert (POST /api/tenant-admin/sso/parse-metadata). The gap is the human fields (contacts, cert expiry, stack type, confirmed mail domains) and a guided first-run wizard. This is the lowest-effort path because the parsing + persistence backend already exists. - Option B β a pre-auth public intake form. A short unauthenticated page
(e.g.
pdf.gevell.com/sso-intakeor a Typeform-style form) their IAM team fills before they have an account, writing to a lightweightsso_intaketable that we review and turn into a tenant. Better for the sales/first-contact stage, before the customer has any logins.
Recommendation: do both, in order β Option B (intake form at first contact) to capture the details, then Option A (the wizard already half-built) for the admin to finish configuration and self-test. The metadata-parse endpoint means we should lean on paste-the-XML wherever possible rather than asking humans to transcribe entity IDs and certs by hand.
Bottom line
- Merged & in prod: the entire SAML feature + DB schema.
- Blocking to ship: PR #2585 (this branch) + set a session timeout.
- To make it a repeatable product: intake form + customer/IdP-admin docs (Phase 2).
- To pass demanding security reviews: SLO, SCIM, enforcement (Phase 3, per deal).