import { createHash, createPublicKey, verify } from "node:crypto"; import { ModelError } from "./model-runtime"; let cached: { issuer: string; expires: number; keys: JsonWebKey[] } | undefined; export async function accessOwner( request: Request, env = process.env ): Promise { const issuer = env.CONTEXT_ACCESS_ISSUER || ""; const audience = env.CONTEXT_ACCESS_AUD || ""; if ( !/^https:\/\/[a-z0-9-]+\.cloudflareaccess\.com$/.test(issuer) || !audience ) throw new ModelError("Reviewer access is not configured yet.", 503); const token = request.headers.get("cf-access-jwt-assertion") || ""; try { if (!token || token.length > 16000) throw new Error(); const parts = token.split("."); if (parts.length !== 3 || !parts.every((p) => /^[A-Za-z0-9_-]+$/.test(p))) throw new Error(); const header = JSON.parse(Buffer.from(parts[0], "base64url").toString()); const claims = JSON.parse(Buffer.from(parts[1], "base64url").toString()); if (header.alg !== "RS256" || typeof header.kid !== "string" || header.crit) throw new Error(); const now = Date.now(); if ( !cached || cached.issuer !== issuer || cached.expires <= now || !cached.keys.some( (k) => (k as JsonWebKey & { kid: string }).kid === header.kid ) ) { const response = await fetch(issuer + "/cdn-cgi/access/certs", { signal: AbortSignal.timeout(5000), redirect: "error", }); if (!response.ok) throw new ModelError( "Unable to verify reviewer sign-in. Retry shortly.", 503 ); const value = await response.json(); if (!Array.isArray(value.keys) || value.keys.length > 20) throw new Error(); cached = { issuer, expires: now + 300000, keys: value.keys }; } const key = cached.keys.find( (k) => (k as JsonWebKey & { kid: string }).kid === header.kid ); if (!key || key.kty !== "RSA") throw new Error(); const valid = verify( "RSA-SHA256", Buffer.from(parts[0] + "." + parts[1]), createPublicKey({ key: key as import("node:crypto").JsonWebKey, format: "jwk", }), Buffer.from(parts[2], "base64url") ); if ( !valid || claims.iss !== issuer || !Array.isArray(claims.aud) || !claims.aud.includes(audience) || typeof claims.exp !== "number" || claims.exp * 1000 <= now || (claims.nbf !== undefined && (typeof claims.nbf !== "number" || claims.nbf * 1000 > now + 30000)) || typeof claims.sub !== "string" || !claims.sub || typeof claims.email !== "string" ) throw new Error(); return createHash("sha256") .update(issuer + ":" + claims.sub) .digest("hex"); } catch (error) { if (error instanceof ModelError) throw error; throw new ModelError( "Reviewer sign-in required. Open https://context.marmar.life and sign in again; saved work remains in this browser.", 401 ); } }