74c7bf6ef7
Previously logout only destroyed our own session -- someone who
signed in via Authentik stayed logged into Authentik itself, so
"Sign in with Authentik" again would silently re-authenticate with
no prompt.
Session now tracks authMethod ("local" | "oidc") and, for OIDC
sessions, the raw id_token (needed as id_token_hint at logout time).
New GET /api/auth/oidc/logout redirects through Authentik's
end_session_endpoint (openid-client's buildEndSessionUrl, not
hand-rolled) before landing back on /. Must be a full-page navigation
-- Authentik needs a real browser request to clear its own session
cookie, a fetch() wouldn't do that. Local sessions still use the
existing POST /api/auth/logout unchanged.
Confirmed Authentik has no dedicated post_logout_redirect_uri
allowlist field by checking the provider's DB schema directly before
implementing, rather than assuming.
Closes #19.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
71 lines
3.0 KiB
TypeScript
71 lines
3.0 KiB
TypeScript
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);
|
|
});
|
|
}
|