149 lines
4.7 KiB
TypeScript
149 lines
4.7 KiB
TypeScript
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 "<empty>";
|
|
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<UserSession | null> {
|
|
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;
|
|
} |