The answer. Cloudflare Access is the fastest way to put a login page in front of an internal tool: an Access application on the hostname (or one click on a workers.dev route), a policy that allows your email domain, and an identity provider. After login every request carries a signed JWT in Cf-Access-Jwt-Assertion. The part people skip is validating that JWT inside the Worker: verify the signature against https://<team>.cloudflareaccess.com/cdn-cgi/access/certs, the issuer, and the application's AUD tag. Without that check, anyone who can reach the Worker directly (a leaked workers.dev URL, a route on another hostname) is "authenticated". For CI and E2E, a Service Token policy (CF-Access-Client-Id and -Secret headers) yields a JWT with common_name instead of an email; a Bypass policy on /health keeps probes unauthenticated.
The pattern.
import { createRemoteJWKSet, jwtVerify } from "jose";
const JWKS = createRemoteJWKSet(new URL(`${env.TEAM_DOMAIN}/cdn-cgi/access/certs`));
const token = request.headers.get("cf-access-jwt-assertion");
if (!token) return new Response("forbidden", { status: 403 });
const { payload } = await jwtVerify(token, JWKS, { issuer: env.TEAM_DOMAIN, audience: env.POLICY_AUD });
const who = (payload.email as string) ?? (payload.common_name as string); // user or service token
TEAM_DOMAIN = https://<team>.cloudflareaccess.com POLICY_AUD = <application audience tag>
Watch out.
- Access does not protect a hostname that no application covers. A Worker with
workers_dev = trueand an Access app only on the custom domain is open onworkers.dev. - Do not create a second Access application for the same hostname to add service tokens; attach a Service Auth policy to the existing one.
- Access is coarse. Roles, step-up MFA and per-record authorization still live in your code; treat Access as the front door.
Related: static-assets-run-worker-first-decides-who-answers · scoped-api-tokens-minted-from-master-token · credential-free-edge-companion-hmac-site-tokens