mirror of
https://github.com/willchen96/mike.git
synced 2026-07-24 23:41:04 +02:00
216 lines
5.8 KiB
TypeScript
216 lines
5.8 KiB
TypeScript
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<typeof console.log>) => {
|
|
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<any, "public", any>,
|
|
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<void> {
|
|
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<void> {
|
|
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();
|
|
}
|