import { Router } from "express"; import { Issuer, generators } from "openid-client"; import config from "../config.js"; import { getLogger } from "../logger.js"; const logger = getLogger("OIDC"); let client; export async function createOidcClient() { const { issuer: issuerUrl, clientId, clientSecret, redirectUri } = config.auth.oidc; if (!issuerUrl || !clientId || !redirectUri) { logger.warn( "OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_REDIRECT_URI not set — OIDC disabled.", ); return null; } const issuer = await Issuer.discover(issuerUrl); client = new issuer.Client({ client_id: clientId, client_secret: clientSecret || undefined, redirect_uris: [redirectUri], response_types: ["code"], }); return client; } export function createOidcRouter(clientUrl) { const router = Router(); router.get("/auth/login", async (req, res, next) => { if (!client) { return res.status(503).send("OIDC not configured"); } try { const nonce = generators.nonce(); const state = generators.state(); const code_verifier = generators.codeVerifier(); const code_challenge = generators.codeChallenge(code_verifier); req.session.oidc = { nonce, state, code_verifier }; const authorizationUrl = client.authorizationUrl({ scope: "openid profile email", state, nonce, code_challenge, code_challenge_method: "S256", }); await new Promise((resolve, reject) => { req.session.save((err) => (err ? reject(err) : resolve())); }); res.redirect(authorizationUrl); } catch (e) { next(e); } }); router.get("/auth/callback", async (req, res, next) => { if (!client) { return res.status(503).send("OIDC not configured"); } try { const params = client.callbackParams(req); const expectedState = req.session.oidc?.state; const checks = { state: expectedState, nonce: req.session.oidc?.nonce, code_verifier: req.session.oidc?.code_verifier, }; const redirectUri = config.auth.oidc.redirectUri; const tokenSet = await client.callback(redirectUri, params, checks); const claims = tokenSet.claims(); req.session.user = { sub: claims.sub, email: claims.email, name: claims.name, }; delete req.session.oidc; await new Promise((resolve, reject) => { req.session.save((err) => (err ? reject(err) : resolve())); }); res.redirect(clientUrl || "/"); } catch (e) { logger.error("callback error:", e); next(e); } }); router.post("/auth/logout", (req, res) => { req.session.destroy(() => { res.json({ ok: true }); }); }); router.get("/auth/logout", (req, res) => { req.session.destroy(() => { res.redirect(clientUrl || "/"); }); }); return router; }