← back to Govarbitrage
src/lib/fleet-sso.ts
45 lines
// Fleet single-sign-on cookie ("aafleet") — a stateless HMAC token shared
// across every *.agentabrams.com site, issued by the fleet-sso service
// (/var/www/fleet-sso/server.mjs). Format: "v1.<exp>.<sig>" where
// sig = HMAC-SHA256(FLEET_SSO_SECRET, "v1." + exp) (hex)
// and exp is a unix-seconds expiry. Because the cookie's Domain is
// ".agentabrams.com", auctions already receives it — validating it here lets a
// single fleet login carry into this app with no second sign-in.
//
// This mirrors the verifier's valid() exactly and is Edge-safe (Web Crypto,
// no Node `crypto`), so it runs inside the Next.js middleware.
export const FLEET_SSO_COOKIE = "aafleet";
const TOKEN_RE = /^v1\.(\d+)\.([0-9a-f]{64})$/;
function toHex(buf: ArrayBuffer): string {
return Array.from(new Uint8Array(buf), (b) => b.toString(16).padStart(2, "0")).join("");
}
// Constant-time-ish compare of two equal-length hex strings.
function safeEqualHex(a: string, b: string): boolean {
if (a.length !== b.length) return false;
let diff = 0;
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
return diff === 0;
}
export async function fleetSsoOk(token: string | undefined | null): Promise<boolean> {
const secret = process.env.FLEET_SSO_SECRET;
if (!secret || !token) return false;
const m = TOKEN_RE.exec(token);
if (!m) return false;
const exp = parseInt(m[1], 10);
if (!(exp > Math.floor(Date.now() / 1000))) return false; // expired
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
const mac = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode("v1." + m[1]));
return safeEqualHex(toHex(mac), m[2]);
}