Enterprise SSO (SAML/OIDC) + SCIM — Ops Runbook
Operator guide to turning on enterprise single sign-on and directory provisioning
for a customer org. The application code is fully shipped (SSO policy,
enforcement gate, SCIM 2.0 endpoints, dashboard config, the signInWithSso
login helper). This runbook is the console + dashboard steps ops performs to
activate it for a specific customer, plus the one small front-end hookup.
Golden rule — order matters. The enforcement gate is inert until you fill Firebase provider id in the dashboard (Step 4). Do Steps 1–3 first; arm in Step 4 last. Arming before the SSO login button exists (Step 3) would lock the customer's users out. The org owner is never locked out, so you always have a recovery path.
0. Prerequisites
- Firebase project on Identity Platform (GCIP). SAML/OIDC providers require the Google Cloud Identity Platform upgrade (Firebase Console → Authentication → "Upgrade to Identity Platform"). This is a billing-tier change — confirm with finance/owner before upgrading. Without it, Step 1 shows no SAML/OIDC option.
- Customer's IdP metadata (from the customer's IT/IdP admin — Okta, Azure AD,
OneLogin, Google Workspace, ADFS, …):
- SAML: IdP Entity ID, IdP SSO URL (sign-in URL), IdP X.509 signing certificate.
- OIDC: Issuer URL, Client ID, Client secret.
- The customer's email domain(s) (e.g.
acme.com) — members on these domains will be forced through SSO. - Admin access to the Pouchy dashboard as the customer org's owner (or an admin), and access to the Firebase Console for the Pouchy project.
The one value the customer's IdP needs from us — the callback / ACS URL:
https://www.pouchy.ai/__/auth/handler
Note the
www.— Pouchy's OAuthauthDomainis deliberatelywww.pouchy.ai(the Apple/Google/SSO callbacks are registered there). Do not use the bare apex for the auth callback. (Everything else developer-facing uses barepouchy.ai; this callback is the single exception.)
1. Register the provider in Firebase (GCIP)
Firebase Console → Authentication → Sign-in method → Add new provider.
SAML
- Choose SAML.
- Name it — Firebase assigns a Provider ID of the form
saml.<name>(e.g.saml.acme). Write this exact id down — you need it in Step 4. - Fill from the customer metadata: IdP Entity ID, IdP SSO URL, Certificate (X.509).
- Callback URL shown by Firebase = the ACS URL you give the customer's IdP.
It should be
https://www.pouchy.ai/__/auth/handler. - Save + Enable the provider.
OIDC
- Choose OpenID Connect.
- Name it — Provider ID is
oidc.<name>(e.g.oidc.acme). Write it down. - Fill Issuer (URL), Client ID, Client secret; grant type "Code flow".
- Callback URL =
https://www.pouchy.ai/__/auth/handler. - Save + Enable.
2. Configure the customer's IdP (their side)
This is the OTHER direction: the values you hand the customer's IdP admin so they can create the Pouchy SAML/OIDC app on their side. (The customer, in turn, hands you their Entity ID / SSO URL / cert for Step 1 — as their IT typically provides via a metadata XML.)
Quick reference — SP-side values to give the customer (this Firebase project)
| Field (their IdP calls it…) | Value |
|---|---|
| ACS URL / Reply URL / Assertion Consumer Service | https://www.pouchy.ai/__/auth/handler |
| SP Entity ID / Audience URI / Identifier | https://mtsocial.firebaseapp.com |
| NameID format | EmailAddress (urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress) |
| Required attribute | email — must be released/mapped (the enforcement gate + member matching key off the email domain) |
These are the values for the
mtsocialFirebase project. Authoritative source: the Firebase provider config screen — when you add the SAML provider in Step 1, Firebase displays the exact Entity ID and ACS/Callback URL it will use; copy those verbatim if they differ from the table (e.g. Firebase may show the…firebaseapp.com/__/auth/handlercallback instead of the custom-domain one — use whatever Firebase shows).
What the customer sends you (for Step 1)
- SAML: their IdP Entity ID, SSO URL (SingleSignOnService Location), and X.509 signing certificate — all present in their IdP's metadata XML (Okta: Applications → the app → Sign On → Identity Provider metadata; Azure/ Entra: Enterprise applications → the app → Single sign-on → SAML, fields "Microsoft Entra Identifier" = Entity ID, "Login URL" = SSO URL, plus the Base64 certificate).
- OIDC: Issuer URL, Client ID, Client secret.
Have the customer's IdP admin add the Pouchy app + assign the relevant users.
3. Ship the SSO login entry point (one-time front-end hookup)
The helper is already in the codebase — signInWithSso(providerId) in
src/lib/services/firebase.ts. A developer wires a "Sign in with SSO" affordance
on the login screen that calls it with the provider id from Step 1:
import { signInWithSso } from '$lib/services/firebase';
// e.g. after the user picks "Sign in with your organization" / enters a work email:
await signInWithSso('saml.acme'); // or 'oidc.acme'
Recommended UX: on the login page, an "Sign in with your organization (SSO)" button that either (a) asks for the work email and looks up the provider, or (b) is shown for known enterprise customers. Minimum viable: a button hard-mapped to the customer's provider id.
This must be deployed before Step 4. Once enforcement is armed, users on the customer's domain can only get in via this path.
4. Arm enforcement in the dashboard (LAST step)
Pouchy Dashboard → People / 成员 → "Enterprise SSO + SCIM" card (signed in as the org owner/admin):
- SSO enabled — on.
- Enforce SSO for allowed domains — on.
- Protocol — SAML or OIDC (matching Step 1).
- Allowed email domains — the customer domain(s), e.g.
acme.com. - Firebase provider id — the exact id from Step 1 (
saml.acme/oidc.acme). This field is the arming switch — enforcement does nothing until it matches the token's sign-in provider. - Save.
The moment you save with a valid provider id, enforcement is live: a non-owner
member on an allowed domain who signs in with a personal Google/Apple/password
account gets 403 sso_required on org access and must use the SSO button.
5. Verify
- Owner path: you (the owner) can still access everything regardless — the owner is permanently exempt.
- Enforced member, wrong path: have a test member on the allowed domain sign
in with personal Google → they should be blocked from the org (403
sso_required). - Enforced member, SSO path: the same member via the SSO button → allowed.
- Non-domain member: a member on a different domain is unaffected (not forced).
If you don't have a customer IdP yet, test with a free sandbox IdP (Okta developer
org, or samltest.id) registered as a throwaway saml.test provider against a test
domain.
6. SCIM directory provisioning (optional, independent of SSO login)
Lets the customer's directory (Okta/Azure AD/OneLogin) auto-create, update, and deactivate Pouchy members. Works today, standalone.
- Dashboard → People → SSO card → Mint SCIM token. Copy the one-time
scim_…token and the Base URL (https://pouchy.ai/scim/v2). Minting rotates any previous token. - In the customer's IdP SCIM/provisioning config:
- SCIM base URL:
https://pouchy.ai/scim/v2 - Auth: HTTP Header / Bearer token = the minted
scim_…token.
- SCIM base URL:
- The IdP will
POST /Users(create → an invited member),PATCH /Users/{id}withactive:false(disable → member removed), andGET /Users?filter=…. - Provisioned members show up in the members table (invited/pending until they
first sign in, then active). SCIM can only touch this org's
org_members— it can never exceed what the owner can do by hand.
To revoke SCIM: mint a new token (invalidates the old one) or turn off SCIM via the config.
7. Rollback / disable
- Disable enforcement without losing config: dashboard → SSO card → turn off Enforce (or SSO enabled), Save. Users can sign in normally again.
- Fully disarm instantly: clear the Firebase provider id field + Save — the gate goes inert (same as never-configured), enforcement stops, no member is blocked. (This is the fastest "unlock everyone" lever.)
- Owner is never affected, so an owner can always reach the dashboard to disarm even mid-incident.
- Removing the Firebase provider in the Console also breaks the SSO login button — disarm in the dashboard first, then remove the provider, to avoid a window where enforced users can neither SSO in nor fall back.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| No SAML/OIDC option in Console | Project not on Identity Platform | Upgrade (Step 0) |
auth/operation-not-allowed on SSO sign-in |
Provider not enabled, or id typo | Enable in Console; ensure the dashboard firebaseProviderId matches the Console id exactly |
| Enforced users can't get in at all | SSO login button not deployed (Step 3) or armed too early | Ship the button; or disarm (clear provider id) until it's live |
| SSO sign-in works but user still blocked | firebaseProviderId in dashboard ≠ token's sign_in_provider |
Make them identical (saml.<name>) |
| Member on domain not forced | domain not in Allowed domains, or Enforce off, or provider id blank | Fix the three arming fields |
| Owner locked out | Not possible by design | If it appears so, it's a different 403 — check the error code |
| SCIM calls 401 | Wrong/rotated token | Re-mint + reconfigure the IdP |
What the platform enforces (for reference)
- Gate lives at the org-access-resolution layer (
requireProjectAccess→ssoAccessBlocked), not a global login wall — it only gates access to the enforcing org's resources. - Never grants access — it can only require SSO for a matching domain.
- Owner exempt, inert until
firebaseProviderIdis set, only enforced domains — three independent safety conditions, all must hold to block.