import { Router } from "express"; import * as client from "openid-client"; import { getOidcConfig } from "./oidc.js"; import { upsertUserFromLogin } from "./users.js"; import { env } from "../env.js"; export const authRouter = Router(); authRouter.get("/login", async (req, res, next) => { try { const config = await getOidcConfig(); const codeVerifier = client.randomPKCECodeVerifier(); const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier); const state = client.randomState(); req.session.pendingAuth = { codeVerifier, state }; const redirectUri = new URL("/auth/callback", env.appBaseUrl).toString(); const authUrl = client.buildAuthorizationUrl(config, { redirect_uri: redirectUri, scope: "openid email profile", code_challenge: codeChallenge, code_challenge_method: "S256", state, }); req.session.save((err) => { if (err) return next(err); res.redirect(authUrl.href); }); } catch (err) { next(err); } }); authRouter.get("/callback", async (req, res, next) => { try { const pending = req.session.pendingAuth; if (!pending) { return res.status(400).send("Login session expired. Please try signing in again."); } const config = await getOidcConfig(); const currentUrl = new URL(req.originalUrl, env.appBaseUrl); const tokens = await client.authorizationCodeGrant(config, currentUrl, { pkceCodeVerifier: pending.codeVerifier, expectedState: pending.state, }); const claims = tokens.claims(); if (!claims?.sub) { return res.status(400).send("Identity provider did not return a valid identity."); } let email: string | undefined = typeof claims.email === "string" ? claims.email : undefined; let name: string | undefined = typeof claims.name === "string" ? claims.name : undefined; try { const userinfo = await client.fetchUserInfo(config, tokens.access_token, claims.sub); email = userinfo.email ?? email; name = userinfo.name ?? userinfo.preferred_username ?? name; } catch { // fall back to ID token claims already captured above } await upsertUserFromLogin({ sub: claims.sub, email, name }); delete req.session.pendingAuth; req.session.user = { sub: claims.sub, email, name, idToken: tokens.id_token }; req.session.save((err) => { if (err) return next(err); res.redirect("/"); }); } catch (err) { next(err); } }); authRouter.get("/logout", async (req, res, next) => { const idToken = req.session.user?.idToken; try { const config = await getOidcConfig(); let endSessionUrl: URL | undefined; try { endSessionUrl = client.buildEndSessionUrl(config, { post_logout_redirect_uri: env.appBaseUrl, ...(idToken ? { id_token_hint: idToken } : {}), }); } catch { // Provider doesn't advertise RP-Initiated Logout; just clear our own session. } req.session.destroy((err) => { if (err) return next(err); res.redirect(endSessionUrl ? endSessionUrl.href : "/"); }); } catch (err) { next(err); } });