BYO SSO ops runbook (hybrid)
Hybrid model: most tenants use default Keycloak (password invite). Selected tenants use byo_sso with Keycloak identity brokering (Google Workspace and/or SAML).
Customer questionnaire
Send before configuring an IdP:
Google Workspace
- Allowed Workspace domain(s) (e.g.
acme.com) - Who creates / approves the Google OAuth client
- Consent / admin approval process
Generic SAML (AD / AD FS / Entra SAML)
- IdP metadata XML or metadata URL
- Preferred NameID format (email recommended)
- Attribute names for email, givenName, surname
- Signing certificate rotation contact
- Whether signed AuthnRequests / encrypted assertions are required
Keycloak (ops)
- Run
npm run keycloak:setup-realm(or CI equivalent) so authentication flowfirst broker login byoexists (Detect Existing Broker User + Automatically Set Existing User, both REQUIRED). Local/pilot may still seed Google IdPacme-googleviaBYO_SSO_GOOGLE_*env vars. - Preferred path: use sequel-app Settings → SSO (super-admin) or
PATCH /api/v1/tenants/{id}/sso-authwith write-only credentials. The API upserts the Keycloak Identity Provider before saving aliases/domains:- Google:
clientId/clientSecreton create; Trust Email ON, Hide on login ON, Sync Mode IMPORT, First login flow =first broker login byo - SAML:
samlMetadataUrl(preferred) orsamlMetadataXmlon create; same BYO advanced defaults - Alias defaults to
{tenantSlug}-google/{tenantSlug}-saml - Response includes handoff: Google redirect URI, SAML Entity ID + ACS URL
- Handoff URLs use the public Keycloak origin (
KEYCLOAK_PUBLIC_URL, else derived fromKEYCLOAK_ISSUER, elseKEYCLOAK_URL). Do not paste Docker-internal hosts (e.g.http://keycloak:8080) into customer IdPs. - Switching to
authMode: defaultdisables (does not delete) the previous IdPs
- Google:
- Leave Keycloak username/password form as the default login (do not set the BYO IdP as realm default).
- Ensure protocol mappers still emit
tenant_idandenvironmenton the access token (same askeycloak-setup-realm.ts). - After first successful SSO for a user, disable their password credential (BYO users should be SSO-only).
Manual Admin Console IdP creation remains available for emergencies; match the same advanced settings as above.
Why users see “Account already exists”
Sequel creates the Keycloak user at invite time (email username, no password). The first Google/SAML login then tries to attach a brokered identity to that pre-created user. Stock Keycloak First Broker Login asks the human to confirm the link (“Review profile” / “Add to existing account”).
- If that screen still appears: choose Add to existing account (that account is the invite).
- Do not choose a path that creates a second Keycloak user for the same email.
First Broker Login — auto-link by email (required for BYO UX)
Goal: invited users go IdP → linked Sequel Keycloak user → app, with no confirmation screen. Still invite-only (never auto-create unknown emails).
Important: Detect Existing Broker User and Automatically Set Existing User are not part of the stock first broker login flow. Duplicating that flow and only disabling “Confirm link…” is not enough — you still hit email/password verify steps. Prefer a new basic flow and add those two authenticators (official Keycloak “detect existing user” pattern).
In Keycloak Admin → realm sequel:
Authentication → Flows → Create flow
- Name: e.g.
first broker login byo - Type: Basic flow
- Name: e.g.
Open the new flow → Add execution (twice), pick these providers (exact labels):
Add execution Requirement Detect Existing Broker User REQUIREDAutomatically Set Existing User REQUIREDOrder: Detect first, then Automatically Set. Do not add Create User If Unique (invite-only). Do not keep Confirm link / Verify by Email / Password re-auth from the stock flow.
Identity Providers →
{tenantSlug}-google(or-saml) → Settings- First login flow override =
first broker login byo(this new flow — not stockfirst broker login). - Sync mode:
IMPORTis fine for pilot; useFORCEonly if you want IdP profile to overwrite Sequel fields on every login. - Trust email:
ONwhen the IdP is authoritative for that org’s mailbox.
- First login flow override =
Smoke-test with a fresh invite:
- Accept invite → org SSO → land in Sequel without “Account already exists”.
- An email that was never invited must still fail (no auto-create).
If you already duplicated stock first broker login, either switch the IdP to the new two-step flow above, or add those two executions into a clean subflow and disable Confirm / Verify Email / Username Password re-auth. Tweaking Confirm alone will not auto-link.
If auto-link is not configured yet, document for support: users should click Add to existing account once; then subsequent logins skip the screen.
Hand off to customer
- SP Entity ID
- ACS URL (Assertion Consumer Service)
- Optional logout URL / SP signing cert
API (super-admin JWT only)
GET/PATCH /api/v1/tenants/{id}/sso-auth require a Keycloak Bearer token for a user with the super-admin realm role. Tenant API keys are rejected (403).
PATCH provisions Keycloak IdPs then stores metadata. Secrets are write-only (never persisted, never returned on GET).
PATCH /api/v1/tenants/{id}/sso-auth
Authorization: Bearer <super-admin JWT>
X-Tenant-Id: {id}
X-Environment: development
{
"authMode": "byo_sso",
"ssoConfig": {
"providers": [
{
"type": "google",
"keycloakAlias": "acme-google",
"domains": ["acme.com"],
"googleClientId": "<oauth-client-id>",
"googleClientSecret": "<oauth-client-secret>"
}
]
}
}
SAML create example:
{
"authMode": "byo_sso",
"ssoConfig": {
"providers": [
{
"type": "saml",
"keycloakAlias": "acme-saml",
"samlMetadataUrl": "https://idp.example.com/metadata"
}
]
}
}
Example with token exchange first:
TOKEN=$(curl -s -X POST 'http://localhost:8000/api/v1/auth/token' \
-H 'Content-Type: application/json' \
-d '{"username":"super-admin@example.com","password":"<super-admin-password>"}' \
| jq -r .access_token)
curl -X PATCH "http://localhost:8000/api/v1/tenants/${TENANT_ID}/sso-auth" \
-H "Authorization: Bearer ${TOKEN}" \
-H "X-Tenant-Id: ${TENANT_ID}" \
-H "X-Environment: development" \
-H "Content-Type: application/json" \
-d '{
"authMode": "byo_sso",
"ssoConfig": {
"providers": [
{
"type": "google",
"domains": ["acme.com"],
"googleClientId": "'"${GOOGLE_CLIENT_ID}"'",
"googleClientSecret": "'"${GOOGLE_CLIENT_SECRET}"'"
}
]
}
}'
To revert (disables IdPs in Keycloak, clears DB metadata):
{ "authMode": "default", "ssoConfig": null }
App login
- Default:
/login→ Keycloak form (nokc_idp_hint) - Returning BYO users (no invite link):
/org-sign-in→ enter slug →/login?tenant={slug} - BYO:
/login?tenant={slug}→ discoversGET /api/v1/auth/tenant-sso?slug=→ authorize withkc_idp_hint - After a successful BYO authorize, sequel-app stores
sequel_last_org_slug(HttpOnly) for sign-out only: BYO →/login?tenant=…&signedOut=1; otherwise → default Keycloak./org-sign-inis manual entry (and clears the cookie), not the default sign-out landing. Visiting/does not apply the cookie. - Pilot fallback env (sequel-app):
BYO_SSO_TENANT_ALIASES=acme:acme-google
Invite flow
- BYO tenants: invite creates Keycloak user without password (username = email, same as default). If the email already exists in this tenant, the membership is linked/reused; if it exists only in another organisation, create is rejected.
- Do not use compound
{tenantId}:{email}usernames — Keycloak rejects:, and email is the stable identity. - Accept invite: no password UI; then redirect to
/login?tenant={slug}. - First SSO: Keycloak should auto-link the IdP identity to that pre-created user (see First Broker Login above). Stock “Account already exists” means the auto-link flow is not assigned yet — choose Add to existing account.
- Assigning an existing user into a BYO tenant is allowed when it is the same organisation (email reuse); cross-tenant email reuse is rejected on create.
Regression checklist
- [ ] Default tenant invite → set password → login works
- [ ] Settings → SSO save for Google creates IdP in Keycloak with BYO advanced settings
- [ ] Settings → SSO save for SAML creates IdP and returns Entity ID / ACS
- [ ] BYO invite → accept → first Google/SAML login without “Account already exists” (auto-link flow)
- [ ] BYO user Google (or SAML) login works;
GET /users/mesucceeds - [ ] Uninvited IdP email cannot create a Keycloak user (invite-only)
- [ ] Super-admin login unchanged
- [ ] Wrong IdP / disabled user fails clearly
- [ ] Switching tenant to
defaultdisables Keycloak IdPs and clears metadata