Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-core/src/access.ts
Snapshot at 34e3175
1import { createRemoteJWKSet, jwtVerify } from "jose";
2import type { Env } from "./types";
3import { HttpError } from "./http";
4
5const keySets = new Map<string, ReturnType<typeof createRemoteJWKSet>>();
6
7export function accessSettings(env: Env) {
8 const issuer = env.CORE_ACCESS_ISSUER;
9 const audience = env.CORE_ACCESS_AUDIENCE;
10 const owners = env.CORE_ACCESS_OWNERS;
11 // Only the configured Access team can supply keys; JWT jku/x5u headers are never followed.
12 if (
13 !issuer ||
14 !/^https:\/\/[a-z0-9-]+\.cloudflareaccess\.com$/.test(issuer) ||
15 !audience?.trim() ||
16 audience !== audience.trim() ||
17 !Array.isArray(owners) ||
18 !owners.length ||
19 !owners.every((email) => typeof email === "string" && /^[^@\s]+@[^@\s]+$/.test(email))
20 ) {
21 throw new HttpError(503, "Owner Access configuration is incomplete.");
22 }
23 return { issuer, audience, owners: owners.map((email) => email.toLowerCase()) };
24}
25
26/** Trust only a signed, current Access application JWT and an explicitly allowed owner. */
27export async function verifyOwner(request: Request, env: Env): Promise<{ subject: string } | null> {
28 const { issuer, audience, owners } = accessSettings(env);
29 // /projects also serves bearer agents, so it is not behind an interactive edge login.
30 // Same-origin owner requests carry the Access application cookie; verify that JWT identically.
31 const cookie = request.headers.get("cookie") ?? "";
32 const token =
33 request.headers.get("cf-access-jwt-assertion") ??
34 (cookie.length <= 32768 ? /(?:^|;\s*)CF_Authorization=([^;]+)/.exec(cookie)?.[1] : undefined);
35 if (!token || token.length > 16384) return null;
36 let keys = keySets.get(issuer);
37 if (!keys) {
38 keys = createRemoteJWKSet(new URL(`${issuer}/cdn-cgi/access/certs`), {
39 timeoutDuration: 5000,
40 cooldownDuration: 30000,
41 cacheMaxAge: 300000,
42 });
43 keySets.set(issuer, keys);
44 }
45 try {
46 const { payload } = await jwtVerify(token, keys, {
47 issuer,
48 audience,
49 algorithms: ["RS256"],
50 requiredClaims: ["sub", "email", "exp", "iat"],
51 });
52 if (
53 typeof payload.sub !== "string" ||
54 !payload.sub ||
55 typeof payload.email !== "string" ||
56 !owners.includes(payload.email.toLowerCase()) ||
57 payload.iat! > Date.now() / 1000
58 )
59 return null;
60 return { subject: payload.sub };
61 } catch {
62 return null;
63 }
64}
65