// PAS Admin Auth Worker // // Secrets (set with `npx wrangler secret put `): // ADMIN_USERNAME, ADMIN_PASSWORD - the admin login // SESSION_SECRET - long random string, used to sign tokens. // The SAME value must be set on the support // worker (and flight worker) so they can // verify tokens issued here. // Optional var: // ALLOWED_ORIGIN - e.g. https://your-site.pages.dev (defaults to *) const enc = new TextEncoder(); const dec = new TextDecoder(); const TOKEN_LIFETIME_SECONDS = 8 * 60 * 60; // 8 hours // ---------- small helpers ---------- function b64u(buffer) { let s = ""; for (const b of new Uint8Array(buffer)) s += String.fromCharCode(b); return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } function fromB64u(str) { const b64 = str.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(str.length / 4) * 4, "="); return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)); } function hmacKey(secret, usages) { return crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, usages); } export async function signToken(secret, payload) { const body = b64u(enc.encode(JSON.stringify(payload))); const sig = await crypto.subtle.sign("HMAC", await hmacKey(secret, ["sign"]), enc.encode(body)); return `${body}.${b64u(sig)}`; } export async function verifyToken(secret, token) { try { const [body, sig] = String(token || "").split("."); if (!body || !sig) return null; const ok = await crypto.subtle.verify("HMAC", await hmacKey(secret, ["verify"]), fromB64u(sig), enc.encode(body)); if (!ok) return null; const payload = JSON.parse(dec.decode(fromB64u(body))); if (!payload.exp || payload.exp < Date.now() / 1000) return null; return payload; } catch { return null; } } // Constant-time string comparison (hash first so lengths don't leak) async function safeEqual(a, b) { const [ha, hb] = await Promise.all([a, b].map((s) => crypto.subtle.digest("SHA-256", enc.encode(String(s ?? ""))))); if (crypto.subtle.timingSafeEqual) return crypto.subtle.timingSafeEqual(ha, hb); const x = new Uint8Array(ha), y = new Uint8Array(hb); let diff = 0; for (let i = 0; i < x.length; i++) diff |= x[i] ^ y[i]; return diff === 0; } function cors(env) { return { "Access-Control-Allow-Origin": env.ALLOWED_ORIGIN || "*", "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization", "Vary": "Origin" }; } function json(env, body, status = 200) { return new Response(JSON.stringify(body), { status, headers: { ...cors(env), "Content-Type": "application/json", "Cache-Control": "no-store" } }); } // ---------- worker ---------- export default { async fetch(request, env) { const url = new URL(request.url); if (request.method === "OPTIONS") { return new Response(null, { status: 204, headers: cors(env) }); } if (!env.SESSION_SECRET || !env.ADMIN_USERNAME || !env.ADMIN_PASSWORD) { return json(env, { success: false, message: "Server not configured" }, 500); } // POST /login { username, password } -> { success, token, expiresIn } if (request.method === "POST" && url.pathname === "/login") { let data; try { data = await request.json(); } catch { return json(env, { success: false, message: "Invalid request" }, 400); } // evaluate both (no short-circuit) so timing doesn't reveal which was wrong const userOk = await safeEqual(data?.username, env.ADMIN_USERNAME); const passOk = await safeEqual(data?.password, env.ADMIN_PASSWORD); if (!(userOk && passOk)) { await new Promise((r) => setTimeout(r, 600)); // slow down brute force return json(env, { success: false, message: "Invalid credentials" }, 401); } const now = Math.floor(Date.now() / 1000); const token = await signToken(env.SESSION_SECRET, { sub: "admin", iat: now, exp: now + TOKEN_LIFETIME_SECONDS }); return json(env, { success: true, token, expiresIn: TOKEN_LIFETIME_SECONDS }); } // POST /verify (Authorization: Bearer ) -> { success } if (request.method === "POST" && url.pathname === "/verify") { const token = (request.headers.get("Authorization") || "").replace(/^Bearer\s+/i, ""); const payload = await verifyToken(env.SESSION_SECRET, token); return payload ? json(env, { success: true }) : json(env, { success: false }, 401); } return new Response("PAS Admin Auth Online", { status: 200, headers: cors(env) }); } };