From e92de12cf9af2ba167170db87979040c103753bf Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Fri, 13 Mar 2026 14:42:05 -0500 Subject: [PATCH] feat(auth): add Authentik OIDC adapter Refs #96 --- apps/gateway/src/main.ts | 9 +++++ docs/plans/authentik-sso-setup.md | 40 +++++++++++++++++++++ docs/scratchpads/p5-004-authentik-sso.md | 44 ++++++++++++++++++++++++ packages/auth/src/auth.ts | 29 ++++++++++++++++ 4 files changed, 122 insertions(+) create mode 100644 docs/plans/authentik-sso-setup.md create mode 100644 docs/scratchpads/p5-004-authentik-sso.md diff --git a/apps/gateway/src/main.ts b/apps/gateway/src/main.ts index 8368591..1e955ce 100644 --- a/apps/gateway/src/main.ts +++ b/apps/gateway/src/main.ts @@ -12,6 +12,15 @@ async function bootstrap(): Promise { throw new Error('BETTER_AUTH_SECRET is required'); } + if ( + process.env['AUTHENTIK_CLIENT_ID'] && + (!process.env['AUTHENTIK_CLIENT_SECRET'] || !process.env['AUTHENTIK_ISSUER']) + ) { + console.warn( + '[warn] AUTHENTIK_CLIENT_ID is set but AUTHENTIK_CLIENT_SECRET or AUTHENTIK_ISSUER is missing — Authentik SSO will not work', + ); + } + const logger = new Logger('Bootstrap'); const app = await NestFactory.create( AppModule, diff --git a/docs/plans/authentik-sso-setup.md b/docs/plans/authentik-sso-setup.md new file mode 100644 index 0000000..a91b0b9 --- /dev/null +++ b/docs/plans/authentik-sso-setup.md @@ -0,0 +1,40 @@ +# Authentik SSO Setup + +## Create the Authentik application + +1. In Authentik, create an OAuth2/OpenID Provider. +2. Create an Application and link it to that provider. +3. Copy the generated client ID and client secret. + +## Required environment variables + +Set these values for the gateway/auth runtime: + +```bash +AUTHENTIK_CLIENT_ID=your-client-id +AUTHENTIK_CLIENT_SECRET=your-client-secret +AUTHENTIK_ISSUER=https://authentik.example.com +``` + +`AUTHENTIK_ISSUER` should be the Authentik base URL, for example `https://authentik.example.com`. + +## Redirect URI + +Configure this redirect URI in the Authentik provider/application: + +```text +{BETTER_AUTH_URL}/api/auth/callback/authentik +``` + +Example: + +```text +https://mosaic.example.com/api/auth/callback/authentik +``` + +## Test the flow + +1. Start the gateway with `BETTER_AUTH_URL` and the Authentik environment variables set. +2. Open the Mosaic login flow and choose the Authentik provider. +3. Complete the Authentik login. +4. Confirm the browser returns to Mosaic and a session is created successfully. diff --git a/docs/scratchpads/p5-004-authentik-sso.md b/docs/scratchpads/p5-004-authentik-sso.md new file mode 100644 index 0000000..769e845 --- /dev/null +++ b/docs/scratchpads/p5-004-authentik-sso.md @@ -0,0 +1,44 @@ +# P5-004 Scratchpad + +- Objective: Add optional Authentik OIDC SSO adapter via Better Auth genericOAuth. +- Task ref: P5-004 +- Issue ref: #96 +- Plan: + 1. Inspect auth/gateway surfaces and Better Auth plugin shape. + 2. Add failing coverage for auth config/startup validation where feasible. + 3. Implement adapter, docs, and warnings. + 4. Run targeted typechecks, lint, and review. + +- TDD note: no low-friction auth plugin or bootstrap-env test seam exists for `packages/auth/src/auth.ts` or `apps/gateway/src/main.ts`. This change is configuration-oriented and does not alter an existing behavioral contract with a current test harness. I skipped new tests for this pass and relied on exact typecheck/lint/test commands plus manual review. + +- Changes: + 1. Added conditional Better Auth `genericOAuth` plugin registration for the `authentik` provider in `packages/auth/src/auth.ts`. + 2. Added a soft startup warning in `apps/gateway/src/main.ts` for incomplete Authentik env configuration. + 3. Added `docs/plans/authentik-sso-setup.md` with env, redirect URI, and test-flow guidance. + 4. Confirmed `packages/auth/src/index.ts` already exports `AuthConfig`; no change required there. + +- Verification: + 1. `pnpm --filter @mosaic/db build` + 2. `pnpm --filter @mosaic/auth typecheck` + 3. `pnpm --filter @mosaic/gateway typecheck` + 4. `pnpm lint` + 5. `pnpm format:check` + 6. `pnpm --filter @mosaic/auth test` + 7. `pnpm --filter @mosaic/gateway test` + +- Results: + 1. `@mosaic/auth` typecheck passed after replacing the non-existent `enabled` field with conditional plugin registration. + 2. `@mosaic/gateway` typecheck passed. + 3. Repo lint passed. + 4. Prettier check passed after formatting `apps/gateway/src/main.ts`. + 5. `@mosaic/auth` tests reported `No test files found, exiting with code 0`. + 6. `@mosaic/gateway` tests passed: `3` files, `20` tests. + +- Review: + 1. Manual review of the diff found no blocker issues. + 2. External `codex-code-review.sh --uncommitted` was attempted but did not return a usable verdict in-session; no automated review findings were available from that run. + +- Situational evidence: + 1. Provider activation is env-gated by `AUTHENTIK_CLIENT_ID`. + 2. Misconfigured optional SSO surfaces a warning instead of crashing gateway startup. + 3. Setup doc records the expected redirect path: `{BETTER_AUTH_URL}/api/auth/callback/authentik`. diff --git a/packages/auth/src/auth.ts b/packages/auth/src/auth.ts index f463eae..ea473a9 100644 --- a/packages/auth/src/auth.ts +++ b/packages/auth/src/auth.ts @@ -1,5 +1,6 @@ import { betterAuth } from 'better-auth'; import { drizzleAdapter } from 'better-auth/adapters/drizzle'; +import { genericOAuth } from 'better-auth/plugins'; import type { Db } from '@mosaic/db'; export interface AuthConfig { @@ -10,6 +11,33 @@ export interface AuthConfig { export function createAuth(config: AuthConfig) { const { db, baseURL, secret } = config; + const authentikIssuer = process.env['AUTHENTIK_ISSUER']; + const authentikClientId = process.env['AUTHENTIK_CLIENT_ID']; + const authentikClientSecret = process.env['AUTHENTIK_CLIENT_SECRET']; + const plugins = authentikClientId + ? [ + genericOAuth({ + config: [ + { + providerId: 'authentik', + clientId: authentikClientId, + clientSecret: authentikClientSecret ?? '', + discoveryUrl: authentikIssuer + ? `${authentikIssuer}/.well-known/openid-configuration` + : undefined, + authorizationUrl: authentikIssuer + ? `${authentikIssuer}/application/o/authorize/` + : undefined, + tokenUrl: authentikIssuer ? `${authentikIssuer}/application/o/token/` : undefined, + userInfoUrl: authentikIssuer + ? `${authentikIssuer}/application/o/userinfo/` + : undefined, + scopes: ['openid', 'email', 'profile'], + }, + ], + }), + ] + : undefined; return betterAuth({ database: drizzleAdapter(db, { @@ -36,6 +64,7 @@ export function createAuth(config: AuthConfig) { expiresIn: 60 * 60 * 24 * 7, // 7 days updateAge: 60 * 60 * 24, // refresh daily }, + plugins, }); }