# 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 | ✅ 32/32 cached green | | lint | ✅ 18/18 cached green | | format:check | ✅ All matched files use Prettier code style | | test (@mosaicstack/auth) | ✅ 8/8 tests passed | ## Verification Evidence - `pnpm typecheck` — FULL TURBO, 32 tasks successful - `pnpm lint` — FULL TURBO, 18 tasks successful - `pnpm format:check` — All matched files use Prettier code style! - `pnpm --filter=@mosaicstack/auth test` — 8 tests passed, 0 failed