Add Authentik OIDC login as an additional sign-in option
CI / web (push) Successful in 19s
CI / api (push) Successful in 28s

Local auth stays the primary/always-available login (don't want to
lock out the saved admin password) — OIDC is additive, shown as a
second button when OIDC_ENABLED=true. Uses openid-client v6 with PKCE.

Authentik-side provider was set up via an authentik blueprint (its own
declarative automation, see docs/oidc-setup.md) rather than touching
any existing admin credentials.

Closes #12.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 20:59:25 -06:00
parent 9891751d37
commit a431b87f1f
12 changed files with 268 additions and 8 deletions
+7 -1
View File
@@ -8,7 +8,13 @@ declare module "@fastify/session" {
}
}
export function registerAuthRoutes(app: FastifyInstance, db: Database.Database): void {
export function registerAuthRoutes(
app: FastifyInstance,
db: Database.Database,
oidcEnabled: boolean
): void {
app.get("/api/auth/config", async () => ({ oidcEnabled }));
app.post<{ Body: { username: string; password: string } }>("/api/auth/login", async (req, reply) => {
const { username, password } = req.body ?? {};
if (!username || !password || !verifyCredentials(db, username, password)) {
+41
View File
@@ -0,0 +1,41 @@
import type { FastifyInstance } from "fastify";
import type * as client from "openid-client";
import { buildLoginRedirect, handleCallback } from "../auth/oidc.js";
declare module "@fastify/session" {
interface FastifySessionObject {
oidcState?: string;
oidcCodeVerifier?: string;
}
}
export function registerOidcRoutes(
app: FastifyInstance,
oidcConfig: client.Configuration,
redirectUri: string
): void {
app.get("/api/auth/oidc/login", async (req, reply) => {
const { url, state, codeVerifier } = await buildLoginRedirect(oidcConfig, redirectUri);
req.session.oidcState = state;
req.session.oidcCodeVerifier = codeVerifier;
return reply.redirect(url);
});
app.get("/api/auth/oidc/callback", async (req, reply) => {
const { oidcState, oidcCodeVerifier } = req.session;
if (!oidcState || !oidcCodeVerifier) {
return reply.code(400).send({ error: "no OIDC login in progress" });
}
try {
const currentUrl = new URL(req.url, `http://${req.headers.host}`);
const { username } = await handleCallback(oidcConfig, currentUrl, oidcState, oidcCodeVerifier);
req.session.username = username;
req.session.oidcState = undefined;
req.session.oidcCodeVerifier = undefined;
return reply.redirect("/");
} catch (err) {
req.log.error({ err }, "OIDC callback failed");
return reply.code(401).send({ error: "OIDC login failed" });
}
});
}