Put Cloudflare Access in front of a Worker, then verify its JWT too

Access authenticates the user and stamps Cf-Access-Jwt-Assertion; verify it against the team certs and app AUD, or a direct call bypasses the login.

Security

· Chapter

40

·

3

min read

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 = true and an Access app only on the custom domain is open on workers.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