import { Request, Response, NextFunction } from "express"; import { createClient, type SupabaseClient } from "@supabase/supabase-js"; import { syncProfileEmail } from "../lib/userLookup"; const isDev = process.env.NODE_ENV !== "production"; const devLog = (...args: Parameters) => { if (isDev) console.log(...args); }; function summarizeMfaFactors( factors: Array<{ factor_type?: string; status?: string; }> | null | undefined, ) { return (factors ?? []).map((factor) => ({ type: factor.factor_type ?? "unknown", status: factor.status ?? "unknown", })); } function isLoginMfaBootstrapRoute(req: Request) { const path = req.originalUrl.split("?")[0]; return ( (req.method === "GET" || req.method === "POST") && (path === "/user/profile" || path === "/users/profile") ); } async function enforceLoginMfaIfEnabled( req: Request, res: Response, admin: SupabaseClient, token: string, ) { if (isLoginMfaBootstrapRoute(req)) return true; const { data, error } = await admin .from("user_profiles") .select("mfa_on_login") .eq("user_id", res.locals.userId) .maybeSingle(); if (error) { devLog("[auth/mfa] login preference lookup failed", { method: req.method, path: req.originalUrl, userId: res.locals.userId, error: error.message, code: error.code, }); if (error.code === "42703") return true; res.status(500).json({ detail: error.message }); return false; } const profile = data as { mfa_on_login?: boolean } | null; if (profile?.mfa_on_login !== true) return true; const { data: assurance, error: assuranceError } = await admin.auth.mfa.getAuthenticatorAssuranceLevel(token); if (assuranceError) { devLog("[auth/mfa] login assurance lookup failed", { method: req.method, path: req.originalUrl, userId: res.locals.userId, error: assuranceError.message, }); res.status(401).json({ detail: assuranceError.message }); return false; } if (assurance.nextLevel === "aal2" && assurance.currentLevel !== "aal2") { devLog("[auth/mfa] login verification required", { method: req.method, path: req.originalUrl, userId: res.locals.userId, }); res.status(403).json({ code: "mfa_verification_required", detail: "MFA verification required", }); return false; } return true; } export async function requireAuth( req: Request, res: Response, next: NextFunction, ): Promise { const auth = req.headers.authorization ?? ""; if (!auth.startsWith("Bearer ")) { res.status(401).json({ detail: "Missing or invalid Authorization header" }); return; } const token = auth.slice(7).trim(); const supabaseUrl = process.env.SUPABASE_URL ?? ""; const serviceKey = process.env.SUPABASE_SECRET_KEY ?? ""; if (!supabaseUrl || !serviceKey) { res.status(500).json({ detail: "Server auth is not configured" }); return; } const admin = createClient(supabaseUrl, serviceKey, { auth: { persistSession: false }, }); const { data } = await admin.auth.getUser(token); if (!data.user) { res.status(401).json({ detail: "Invalid or expired token" }); return; } res.locals.userId = data.user.id; res.locals.userEmail = data.user.email?.toLowerCase() ?? ""; res.locals.token = token; const syncError = await syncProfileEmail( admin, data.user.id, data.user.email, ); if (syncError) { devLog("[auth/profile-email] sync failed", { method: req.method, path: req.originalUrl, userId: data.user.id, error: syncError.message, }); } if (!(await enforceLoginMfaIfEnabled(req, res, admin, token))) { return; } next(); } export async function requireMfaIfEnrolled( req: Request, res: Response, next: NextFunction, ): Promise { const token = typeof res.locals.token === "string" ? res.locals.token : ""; if (!token) { devLog("[auth/mfa] missing auth session", { method: req.method, path: req.originalUrl, }); res.status(401).json({ detail: "Missing auth session" }); return; } const supabaseUrl = process.env.SUPABASE_URL ?? ""; const serviceKey = process.env.SUPABASE_SECRET_KEY ?? ""; if (!supabaseUrl || !serviceKey) { res.status(500).json({ detail: "Server auth is not configured" }); return; } const admin = createClient(supabaseUrl, serviceKey, { auth: { persistSession: false }, }); const { data, error } = await admin.auth.mfa.getAuthenticatorAssuranceLevel(token); if (error) { devLog("[auth/mfa] assurance lookup failed", { method: req.method, path: req.originalUrl, userId: res.locals.userId, error: error.message, }); res.status(401).json({ detail: error.message }); return; } devLog("[auth/mfa] assurance level", { method: req.method, path: req.originalUrl, userId: res.locals.userId, currentLevel: data.currentLevel, nextLevel: data.nextLevel, required: data.nextLevel === "aal2" && data.currentLevel !== "aal2", }); if (isDev) { const { data: userData, error: userError } = await admin.auth.getUser(token); devLog("[auth/mfa] user factors", { method: req.method, path: req.originalUrl, userId: res.locals.userId, factorCount: userData.user?.factors?.length ?? 0, factors: summarizeMfaFactors(userData.user?.factors), error: userError?.message ?? null, }); } if (data.nextLevel === "aal2" && data.currentLevel !== "aal2") { devLog("[auth/mfa] verification required", { method: req.method, path: req.originalUrl, userId: res.locals.userId, }); res.status(403).json({ code: "mfa_verification_required", detail: "MFA verification required", }); return; } next(); }