QFind anything/
API Reference

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)

  1. Run npm run keycloak:setup-realm (or CI equivalent) so authentication flow first broker login byo exists (Detect Existing Broker User + Automatically Set Existing User, both REQUIRED). Local/pilot may still seed Google IdP acme-google via BYO_SSO_GOOGLE_* env vars.
  2. Preferred path: use sequel-app Settings → SSO (super-admin) or PATCH /api/v1/tenants/{id}/sso-auth with write-only credentials. The API upserts the Keycloak Identity Provider before saving aliases/domains:
    • Google: clientId / clientSecret on create; Trust Email ON, Hide on login ON, Sync Mode IMPORT, First login flow = first broker login byo
    • SAML: samlMetadataUrl (preferred) or samlMetadataXml on 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 from KEYCLOAK_ISSUER, else KEYCLOAK_URL). Do not paste Docker-internal hosts (e.g. http://keycloak:8080) into customer IdPs.
    • Switching to authMode: default disables (does not delete) the previous IdPs
  3. Leave Keycloak username/password form as the default login (do not set the BYO IdP as realm default).
  4. Ensure protocol mappers still emit tenant_id and environment on the access token (same as keycloak-setup-realm.ts).
  5. 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.

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:

  1. Authentication → Flows → Create flow

    • Name: e.g. first broker login byo
    • Type: Basic flow
  2. Open the new flow → Add execution (twice), pick these providers (exact labels):

    Add executionRequirement
    Detect Existing Broker UserREQUIRED
    Automatically Set Existing UserREQUIRED

    Order: 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.

  3. Identity Providers → {tenantSlug}-google (or -saml) → Settings

    • First login flow override = first broker login byo (this new flow — not stock first broker login).
    • Sync mode: IMPORT is fine for pilot; use FORCE only if you want IdP profile to overwrite Sequel fields on every login.
    • Trust email: ON when the IdP is authoritative for that org’s mailbox.
  4. 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 (no kc_idp_hint)
  • Returning BYO users (no invite link): /org-sign-in → enter slug → /login?tenant={slug}
  • BYO: /login?tenant={slug} → discovers GET /api/v1/auth/tenant-sso?slug= → authorize with kc_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-in is 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/me succeeds
  • [ ] Uninvited IdP email cannot create a Keycloak user (invite-only)
  • [ ] Super-admin login unchanged
  • [ ] Wrong IdP / disabled user fails clearly
  • [ ] Switching tenant to default disables Keycloak IdPs and clears metadata