From 254da353003a8b50f0519baf4b6b6e6b58d2a398 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Wed, 18 Mar 2026 21:17:11 -0500 Subject: [PATCH 1/4] feat(auth): add WorkOS + Keycloak SSO providers (P8-001) - Refactor auth.ts to build OAuth providers array dynamically; extract buildOAuthProviders() for unit-testability - Add WorkOS provider (WORKOS_CLIENT_ID/SECRET/REDIRECT_URI env vars) - Add Keycloak provider with realm-scoped OIDC discovery (KEYCLOAK_URL/REALM/CLIENT_ID/CLIENT_SECRET env vars) - Add genericOAuthClient plugin to web auth-client for signIn.oauth2() - Add WorkOS + Keycloak SSO buttons to login page (NEXT_PUBLIC_*_ENABLED feature flags control visibility) - Update .env.example with SSO provider stanzas - Add 8 unit tests covering all provider inclusion/exclusion paths Co-Authored-By: Claude Sonnet 4.6 --- .env.example | 19 +++- apps/web/src/app/(auth)/login/page.tsx | 46 +++++++++- apps/web/src/lib/auth-client.ts | 4 +- packages/auth/src/auth.test.ts | 115 +++++++++++++++++++++++++ packages/auth/src/auth.ts | 82 ++++++++++++------ 5 files changed, 235 insertions(+), 31 deletions(-) create mode 100644 packages/auth/src/auth.test.ts diff --git a/.env.example b/.env.example index 4cbd157..7132b24 100644 --- a/.env.example +++ b/.env.example @@ -123,7 +123,24 @@ OTEL_SERVICE_NAME=mosaic-gateway # TELEGRAM_GATEWAY_URL=http://localhost:4000 -# ─── Authentik SSO (optional — set AUTHENTIK_CLIENT_ID to enable) ──────────── +# ─── SSO Providers (add credentials to enable) ─────────────────────────────── + +# --- Authentik (optional — set AUTHENTIK_CLIENT_ID to enable) --- # AUTHENTIK_ISSUER=https://auth.example.com/application/o/mosaic/ # AUTHENTIK_CLIENT_ID= # AUTHENTIK_CLIENT_SECRET= + +# --- WorkOS (optional — set WORKOS_CLIENT_ID to enable) --- +# WORKOS_CLIENT_ID=client_... +# WORKOS_CLIENT_SECRET=sk_live_... +# WORKOS_REDIRECT_URI=http://localhost:3000/api/auth/callback/workos + +# --- Keycloak (optional — set KEYCLOAK_CLIENT_ID to enable) --- +# KEYCLOAK_URL=https://auth.example.com +# KEYCLOAK_REALM=master +# KEYCLOAK_CLIENT_ID=mosaic +# KEYCLOAK_CLIENT_SECRET= + +# Feature flags — set to true alongside provider credentials to show SSO buttons in the UI +# NEXT_PUBLIC_WORKOS_ENABLED=true +# NEXT_PUBLIC_KEYCLOAK_ENABLED=true diff --git a/apps/web/src/app/(auth)/login/page.tsx b/apps/web/src/app/(auth)/login/page.tsx index b01d04f..836e062 100644 --- a/apps/web/src/app/(auth)/login/page.tsx +++ b/apps/web/src/app/(auth)/login/page.tsx @@ -5,6 +5,10 @@ import { useRouter } from 'next/navigation'; import Link from 'next/link'; import { signIn } from '@/lib/auth-client'; +const workosEnabled = process.env['NEXT_PUBLIC_WORKOS_ENABLED'] === 'true'; +const keycloakEnabled = process.env['NEXT_PUBLIC_KEYCLOAK_ENABLED'] === 'true'; +const hasSsoProviders = workosEnabled || keycloakEnabled; + export default function LoginPage(): React.ReactElement { const router = useRouter(); const [error, setError] = useState(null); @@ -30,6 +34,16 @@ export default function LoginPage(): React.ReactElement { router.push('/chat'); } + async function handleSsoSignIn(providerId: string): Promise { + setError(null); + setLoading(true); + const result = await signIn.oauth2({ providerId, callbackURL: '/chat' }); + if (result?.error) { + setError(result.error.message ?? 'SSO sign in failed'); + setLoading(false); + } + } + return (

Sign in

@@ -44,7 +58,37 @@ export default function LoginPage(): React.ReactElement {
)} -
+ {hasSsoProviders && ( +
+ {workosEnabled && ( + + )} + {keycloakEnabled && ( + + )} +
+
+ or +
+
+
+ )} + +