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 OAuth authDomain is deliberately www.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 bare pouchy.ai; this callback is the single exception.)


1. Register the provider in Firebase (GCIP)

Firebase Console → Authentication → Sign-in method → Add new provider.

SAML

  1. Choose SAML.
  2. 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.
  3. Fill from the customer metadata: IdP Entity ID, IdP SSO URL, Certificate (X.509).
  4. Callback URL shown by Firebase = the ACS URL you give the customer's IdP. It should be https://www.pouchy.ai/__/auth/handler.
  5. Save + Enable the provider.

OIDC

  1. Choose OpenID Connect.
  2. Name it — Provider ID is oidc.<name> (e.g. oidc.acme). Write it down.
  3. Fill Issuer (URL), Client ID, Client secret; grant type "Code flow".
  4. Callback URL = https://www.pouchy.ai/__/auth/handler.
  5. 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 mtsocial Firebase 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/handler callback 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):

  1. SSO enabled — on.
  2. Enforce SSO for allowed domains — on.
  3. Protocol — SAML or OIDC (matching Step 1).
  4. Allowed email domains — the customer domain(s), e.g. acme.com.
  5. 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.
  6. 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

  1. Owner path: you (the owner) can still access everything regardless — the owner is permanently exempt.
  2. 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).
  3. Enforced member, SSO path: the same member via the SSO button → allowed.
  4. 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.

  1. 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.
  2. 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.
  3. The IdP will POST /Users (create → an invited member), PATCH /Users/{id} with active:false (disable → member removed), and GET /Users?filter=….
  4. 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 (requireProjectAccessssoAccessBlocked), 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 firebaseProviderId is set, only enforced domains — three independent safety conditions, all must hold to block.