import jwt from "jsonwebtoken"; import {UserSession} from "@/types/auth"; import {Claims} from "@/types/auth"; const DEBUG_TOKEN = (process.env.DEBUG_TOKEN ?? "true").toLowerCase() === "true"; function redactToken(t?: string) { if (!t) return ""; const raw = t.startsWith("Bearer ") ? t.slice(7) : t; if (raw.length <= 12) return raw.replace(/./g, "*"); return `${raw.slice(0, 6)}…${raw.slice(-6)}`; } function parseBearer(input: string) { return input?.startsWith("Bearer ") ? input.slice(7) : input; } function decodeBase64UrlPart(part: string): unknown { try { const pad = part.length % 4 === 2 ? "==" : part.length % 4 === 3 ? "=" : ""; const s = part.replace(/-/g, "+").replace(/_/g, "/") + pad; const json = Buffer.from(s, "base64").toString("utf8"); return JSON.parse(json); } catch { return null; } } export async function getUserFromToken( token: string, ): Promise { const input = parseBearer(token); if (DEBUG_TOKEN) { console.log("[server-auth] getUserFromToken() input:", { tokenPreview: redactToken(token), hasBearerPrefix: token?.startsWith?.("Bearer "), }); } if (!input) { if (DEBUG_TOKEN) console.warn("[server-auth] no token provided"); return null; } const HS_SECRET = process.env.JWT_SECRET; const RS_PUBLIC = process.env.JWT_PUBLIC_KEY; let decoded: Claims | null = null; let headerAlg: string | undefined; try { const [hPart] = input.split("."); const hdr = decodeBase64UrlPart(hPart) as { alg?: string } | null; headerAlg = hdr?.alg; if (HS_SECRET) { decoded = jwt.verify(input, HS_SECRET, { algorithms: ["HS256", "HS384", "HS512"], clockTolerance: 5, }) as Claims; if (DEBUG_TOKEN) console.log("[server-auth] verified with HS secret", {alg: headerAlg}); } else if (RS_PUBLIC) { decoded = jwt.verify(input, RS_PUBLIC, { algorithms: ["RS256", "RS384", "RS512"], clockTolerance: 5, }) as Claims; if (DEBUG_TOKEN) console.log("[server-auth] verified with RS public key", { alg: headerAlg, }); } else { decoded = jwt.decode(input) as Claims | null; if (DEBUG_TOKEN) console.warn("[server-auth] decoded WITHOUT verification", { alg: headerAlg, }); } } catch (e: unknown) { const err = e as Error; if (DEBUG_TOKEN) { console.error("[server-auth] verify/decode failed:", { message: err.message, name: err.name, alg: headerAlg, }); } try { const parts = input.split("."); if (parts.length >= 2) { decoded = decodeBase64UrlPart(parts[1]) as Claims | null; if (DEBUG_TOKEN) console.warn("[server-auth] manual payload decode fallback used"); } } catch { /* ignore */ } } if (!decoded) { if (DEBUG_TOKEN) console.warn("[server-auth] decoded = null"); return null; } const now = Math.floor(Date.now() / 1000); if (typeof decoded.nbf === "number" && now + 5 < decoded.nbf) { if (DEBUG_TOKEN) console.warn("[server-auth] token not yet valid", {now, nbf: decoded.nbf}); } if (typeof decoded.exp === "number" && now - 5 > decoded.exp) { if (DEBUG_TOKEN) console.warn("[server-auth] token expired", {now, exp: decoded.exp}); } const user: UserSession = { id: decoded.sub ?? "", email: decoded.email, tenantId: decoded.tenant_id, tenantKey: decoded.tenant_key ?? decoded.tenantKey, roles: Array.isArray(decoded.roles) ? decoded.roles : [], _dbg: DEBUG_TOKEN ? { alg: headerAlg, hasExp: typeof decoded.exp === "number", hasNbf: typeof decoded.nbf === "number", iss: decoded.iss, aud: decoded.aud, sid: decoded.sid, jti: decoded.jti, } : undefined, }; if (DEBUG_TOKEN) { console.log("[server-auth] user parsed:", { id: user.id, email: user.email, tenantId: user.tenantId, tenantKey: user.tenantKey, roles: user.roles, alg: user._dbg?.alg, }); } if (!user.id && DEBUG_TOKEN) console.warn("[server-auth] missing sub"); return user; }