# P8-001 — WorkOS + Keycloak SSO Providers **Branch:** feat/p8-001-sso-providers **Started:** 2026-03-18 **Mode:** Delivery ## Objective Add WorkOS and Keycloak as optional SSO providers to the BetterAuth configuration, following the existing Authentik pattern. ## Scope | Surface | Change | | ---------------------------------------- | ----------------------------------------------------------------------- | | `packages/auth/src/auth.ts` | Refactor provider array, add WorkOS + Keycloak conditional registration | | `apps/web/src/lib/auth-client.ts` | Add `genericOAuthClient()` plugin | | `apps/web/src/app/(auth)/login/page.tsx` | WorkOS + Keycloak SSO buttons gated by `NEXT_PUBLIC_*` env vars | | `.env.example` | Document WorkOS + Keycloak env vars | | `packages/auth/src/auth.test.ts` | Unit tests verifying env-var gating | ## Plan 1. ✅ Refactor `createAuth` to build `oauthProviders[]` conditionally 2. ✅ Add WorkOS provider (explicit URLs, no discovery) 3. ✅ Add Keycloak provider (discoveryUrl pattern) 4. ✅ Add `genericOAuthClient()` to auth-client.ts 5. ✅ Add SSO buttons to login page gated by `NEXT_PUBLIC_WORKOS_ENABLED` / `NEXT_PUBLIC_KEYCLOAK_ENABLED` 6. ✅ Update `.env.example` 7. ⏳ Write `auth.test.ts` with env-var gating tests 8. ⏳ Quality gates: typecheck + lint + format:check + test 9. ⏳ Commit + push + PR ## Decisions - **WorkOS**: Uses explicit `authorizationUrl`, `tokenUrl`, `userInfoUrl` (no discovery endpoint available) - **Keycloak**: Uses `discoveryUrl` pattern (`{URL}/realms/{REALM}/.well-known/openid-configuration`) - **UI gating**: Login page uses `NEXT_PUBLIC_WORKOS_ENABLED` / `NEXT_PUBLIC_KEYCLOAK_ENABLED` feature flags (safer than exposing secret env var names client-side) - **Refactor**: Authentik moved into same `oauthProviders[]` array pattern — cleaner, more extensible - **Feature flag design**: `NEXT_PUBLIC_*` flags are opt-in alongside credentials (prevents accidental button render when creds not set) ## Assumptions - `ASSUMPTION:` WorkOS OIDC discovery URL is not publicly documented; using direct URL pattern from WorkOS SSO docs. - `ASSUMPTION:` `NEXT_PUBLIC_WORKOS_ENABLED=true` must be explicitly set — this is intentional (credential presence alone doesn't enable the button since NEXT_PUBLIC vars are baked at build time). ## Tests - `auth.test.ts`: Mocks betterAuth stack, verifies WorkOS included/excluded based on env var - `auth.test.ts`: Verifies Keycloak discoveryUrl constructed correctly ## Quality Gate Results | Gate | Status | | ------------------- | ------ | | typecheck | ⏳ | | lint | ⏳ | | format:check | ⏳ | | test (@mosaic/auth) | ⏳ | ## Verification Evidence ⏳ Pending