import crypto from "node:crypto"; const SESSION_COOKIE = "hf_admin_session"; const STATE_COOKIE = "hf_admin_state"; const OIDC_CACHE = new Map(); function sign(value, secret) { return crypto.createHmac("sha256", String(secret || "")).update(value).digest("base64url"); } function encodePayload(payload) { return Buffer.from(JSON.stringify(payload), "utf8").toString("base64url"); } function decodePayload(value) { return JSON.parse(Buffer.from(String(value || ""), "base64url").toString("utf8")); } function serializeCookie(name, value, options = {}) { const parts = [`${name}=${value}`]; parts.push(`Path=${options.path || "/"}`); if (options.httpOnly !== false) parts.push("HttpOnly"); if (options.secure !== false) parts.push("Secure"); if (options.sameSite) parts.push(`SameSite=${options.sameSite}`); if (options.maxAge != null) parts.push(`Max-Age=${options.maxAge}`); if (options.expires) parts.push(`Expires=${options.expires.toUTCString()}`); return parts.join("; "); } function parseCookies(req) { const raw = String(req.headers.cookie || ""); const cookies = {}; for (const part of raw.split(";")) { const trimmed = part.trim(); if (!trimmed) continue; const idx = trimmed.indexOf("="); if (idx <= 0) continue; cookies[trimmed.slice(0, idx)] = decodeURIComponent(trimmed.slice(idx + 1)); } return cookies; } function resolveBaseUrl(req) { const forwardedProto = String(req.headers["x-forwarded-proto"] || "").split(",")[0].trim(); const forwardedHost = String(req.headers["x-forwarded-host"] || "").split(",")[0].trim(); const proto = forwardedProto || "https"; const host = forwardedHost || String(req.headers.host || ""); return `${proto}://${host}`; } function allowedUsersFromEnv(env) { return new Set( String(env.ADMIN_HF_USERNAMES || "") .split(/[,\s]+/) .map((item) => item.trim()) .filter(Boolean), ); } function decodeJwtPayload(token) { try { const parts = String(token || "").split("."); if (parts.length < 2) return null; return JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8")); } catch { return null; } } export function createSignedToken(payload, secret) { const encoded = encodePayload(payload); return `${encoded}.${sign(encoded, secret)}`; } export function verifySignedToken(token, secret) { const [encoded, signature] = String(token || "").split("."); if (!encoded || !signature) return null; const expected = sign(encoded, secret); const left = Buffer.from(signature); const right = Buffer.from(expected); if (left.length !== right.length || !crypto.timingSafeEqual(left, right)) { return null; } const payload = decodePayload(encoded); if (payload?.exp && Number(payload.exp) < Date.now()) { return null; } return payload; } export function createAdminSessionCookie(user, secret) { const token = createSignedToken( { username: user.username, displayName: user.displayName || user.username, exp: Date.now() + 8 * 60 * 60 * 1000, }, secret, ); return serializeCookie(SESSION_COOKIE, token, { sameSite: "Lax", maxAge: 8 * 60 * 60, }); } export function clearAdminSessionCookie() { return serializeCookie(SESSION_COOKIE, "", { sameSite: "Lax", maxAge: 0, expires: new Date(0), }); } export function getAdminSession(req, secret) { const cookies = parseCookies(req); const payload = verifySignedToken(cookies[SESSION_COOKIE], secret); if (!payload?.username) return null; return { username: payload.username, displayName: payload.displayName || payload.username, }; } export function getLocalBypassAdminSession(env = process.env) { if (String(env.HF_LOCAL_BYPASS_AUTH || "") !== "1") { return null; } const username = String(env.HF_LOCAL_BYPASS_USER || "").trim() || String(env.ADMIN_HF_USERNAMES || "").split(/[,\s]+/).map((item) => item.trim()).find(Boolean) || "local-admin"; return { username, displayName: env.HF_LOCAL_BYPASS_DISPLAY_NAME || username, }; } export function isAdminUser(session, env) { if (!session?.username) return false; const allowed = allowedUsersFromEnv(env); return allowed.size === 0 ? false : allowed.has(session.username); } async function getOidcConfiguration(providerUrl) { if (OIDC_CACHE.has(providerUrl)) { return OIDC_CACHE.get(providerUrl); } const response = await fetch(`${providerUrl.replace(/\/+$/, "")}/.well-known/openid-configuration`); if (!response.ok) { throw new Error(`Failed to load OpenID configuration: ${response.status}`); } const config = await response.json(); OIDC_CACHE.set(providerUrl, config); return config; } export async function beginAdminOAuth(req, res, env = process.env) { const providerUrl = env.OPENID_PROVIDER_URL; const clientId = env.OAUTH_CLIENT_ID; const clientSecret = env.OAUTH_CLIENT_SECRET; const sessionSecret = env.ADMIN_SESSION_SECRET; if (!providerUrl || !clientId || !clientSecret) { res.statusCode = 404; res.end("OAuth disabled"); return; } if (!sessionSecret) { throw new Error("ADMIN_SESSION_SECRET is required."); } const config = await getOidcConfiguration(providerUrl); const stateToken = createSignedToken( { nonce: crypto.randomUUID(), exp: Date.now() + 10 * 60 * 1000, }, sessionSecret, ); const redirectUri = `${resolveBaseUrl(req)}/login/callback`; const authUrl = new URL(config.authorization_endpoint); authUrl.searchParams.set("client_id", clientId); authUrl.searchParams.set("redirect_uri", redirectUri); authUrl.searchParams.set("response_type", "code"); authUrl.searchParams.set("scope", env.OAUTH_SCOPES || env.HF_OAUTH_SCOPES || "openid profile"); authUrl.searchParams.set("state", stateToken); res.statusCode = 302; res.setHeader("set-cookie", serializeCookie(STATE_COOKIE, stateToken, { sameSite: "Lax", maxAge: 10 * 60, })); res.setHeader("location", authUrl.toString()); res.end(); } export async function finishAdminOAuth(req, res, env = process.env) { const providerUrl = env.OPENID_PROVIDER_URL; const clientId = env.OAUTH_CLIENT_ID; const clientSecret = env.OAUTH_CLIENT_SECRET; const sessionSecret = env.ADMIN_SESSION_SECRET; const config = await getOidcConfiguration(providerUrl); const url = new URL(req.url || "/", resolveBaseUrl(req)); const code = url.searchParams.get("code") || ""; const state = url.searchParams.get("state") || ""; const cookies = parseCookies(req); const storedState = cookies[STATE_COOKIE] || ""; if (!code || state !== storedState || !verifySignedToken(state, sessionSecret)) { res.statusCode = 401; res.end("Invalid OAuth state."); return; } const redirectUri = `${resolveBaseUrl(req)}/login/callback`; const tokenResponse = await fetch(config.token_endpoint, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "authorization_code", code, client_id: clientId, client_secret: clientSecret, redirect_uri: redirectUri, }).toString(), }); if (!tokenResponse.ok) { res.statusCode = 502; res.end("OAuth token exchange failed."); return; } const tokenPayload = await tokenResponse.json(); let profile = null; if (config.userinfo_endpoint && tokenPayload.access_token) { const userinfoResponse = await fetch(config.userinfo_endpoint, { headers: { authorization: `Bearer ${tokenPayload.access_token}`, }, }); if (userinfoResponse.ok) { profile = await userinfoResponse.json(); } } if (!profile && tokenPayload.id_token) { profile = decodeJwtPayload(tokenPayload.id_token); } const username = profile?.preferred_username || profile?.nickname || profile?.name || profile?.sub || ""; const session = { username, displayName: profile?.name || username }; if (!isAdminUser(session, env)) { res.statusCode = 403; res.end("Forbidden"); return; } res.statusCode = 302; res.setHeader("set-cookie", [ createAdminSessionCookie(session, sessionSecret), serializeCookie(STATE_COOKIE, "", { sameSite: "Lax", maxAge: 0, expires: new Date(0), }), ]); res.setHeader("location", "/admin"); res.end(); }