Merge pull request 'feat(auth): P5-004 Authentik OIDC adapter via Better Auth genericOAuth' (#97) from feat/p5-sso-authentik into main
Some checks failed
ci/woodpecker/push/ci Pipeline failed

This commit was merged in pull request #97.
This commit is contained in:
2026-03-13 20:15:50 +00:00
4 changed files with 122 additions and 0 deletions

View File

@@ -12,6 +12,15 @@ async function bootstrap(): Promise<void> {
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<NestFastifyApplication>(
AppModule,

View File

@@ -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.

View File

@@ -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`.

View File

@@ -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,
});
}