import type { FastifyInstance } from "fastify"; import type * as client from "openid-client"; import { buildLoginRedirect, buildLogoutRedirect, 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 { // Same guaranteed-correct-origin trick as the callback's currentUrl below -- // avoids re-deriving scheme/host from headers Fastify can't see accurately. const postLogoutRedirectUri = new URL("/", redirectUri).href; 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 { // Fastify only ever sees plain HTTP here -- TLS terminates at NPM/Cloudflare // before reaching this process (see docs/oidc-setup.md). Building this URL // from req.headers.host with a hardcoded "http://" sent the wrong scheme in // the token exchange's redirect_uri, which Authentik rejects as a mismatch // against the https:// URL registered for this provider. Since redirectUri // is guaranteed correct (it's the exact value used to build the original // authorize request), reuse its origin and only take the query string from // the actual incoming request. const currentUrl = new URL(redirectUri); currentUrl.search = new URL(req.url, "http://placeholder").search; const { username, idToken } = await handleCallback(oidcConfig, currentUrl, oidcState, oidcCodeVerifier); req.session.username = username; req.session.authMethod = "oidc"; req.session.oidcIdToken = idToken; 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" }); } }); // Full-page navigation, not a fetch -- Authentik needs to see this as a real // browser request to clear its own session cookie on auth.jerodrigged.com // before redirecting back. Only meaningful for sessions actually // established via OIDC; local sessions fall back to plain local logout. app.get("/api/auth/oidc/logout", async (req, reply) => { if (req.session.authMethod !== "oidc") { await req.session.destroy(); return reply.redirect("/"); } const logoutUrl = buildLogoutRedirect(oidcConfig, req.session.oidcIdToken, postLogoutRedirectUri); await req.session.destroy(); return reply.redirect(logoutUrl); }); }