diff --git a/.dev.vars.example b/.dev.vars.example index f0fd0e1..f0c881b 100644 --- a/.dev.vars.example +++ b/.dev.vars.example @@ -1,3 +1,3 @@ -# Cloudflare Access (required in production) +# Cloudflare Access (required in production). TEAM_DOMAIN may be the base Access URL or the full /cdn-cgi/access/certs URL. POLICY_AUD=your-access-policy-audience-tag TEAM_DOMAIN=https://your-team.cloudflareaccess.com diff --git a/README.md b/README.md index 2a897b6..8de25da 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Click the button above to deploy to your Cloudflare account. The deploy flow wil 1. **Set up Email Routing** -- In the Cloudflare dashboard, go to your domain > Email Routing and create a catch-all rule that forwards to this Worker 2. **Enable Email Service** -- The worker needs the `send_email` binding to send outbound emails. See [Email Service docs](https://developers.cloudflare.com/email-routing/email-workers/send-email-workers/) 3. **Create a mailbox** -- Visit your deployed app and create a mailbox for any address on your domain (e.g. `hello@example.com`) -4. **Configure Cloudflare Access** -- Enable [one-click Cloudflare Access](https://developers.cloudflare.com/changelog/post/2025-10-03-one-click-access-for-workers/) on your Worker under Settings > Domains & Routes. The modal will show your `POLICY_AUD` and `TEAM_DOMAIN` values. **You must set these are secrets for your Worker.** +4. **Configure Cloudflare Access** -- Enable [one-click Cloudflare Access](https://developers.cloudflare.com/changelog/post/2025-10-03-one-click-access-for-workers/) on your Worker under Settings > Domains & Routes. The modal will show your `POLICY_AUD` and `TEAM_DOMAIN` values. `TEAM_DOMAIN` can be either your Access team URL or the full `.../cdn-cgi/access/certs` URL. **You must set these as secrets for your Worker.** ## Features diff --git a/workers/app.ts b/workers/app.ts index 96cc7e0..607525f 100644 --- a/workers/app.ts +++ b/workers/app.ts @@ -28,6 +28,17 @@ const requestHandler = createRequestHandler( import.meta.env.MODE, ); +function getAccessUrls(teamDomain: string) { + const certsPath = "/cdn-cgi/access/certs"; + const teamUrl = new URL(teamDomain); + const issuer = teamUrl.origin; + const certsUrl = teamUrl.pathname.endsWith(certsPath) + ? teamUrl + : new URL(certsPath, issuer); + + return { issuer, certsUrl }; +} + // Main app that wraps the API and adds React Router fallback const app = new Hono<{ Bindings: Env }>(); @@ -54,11 +65,10 @@ app.use("*", async (c, next) => { } try { - const JWKS = createRemoteJWKSet( - new URL(`${TEAM_DOMAIN}/cdn-cgi/access/certs`), - ); + const { issuer, certsUrl } = getAccessUrls(TEAM_DOMAIN); + const JWKS = createRemoteJWKSet(certsUrl); await jwtVerify(token, JWKS, { - issuer: TEAM_DOMAIN, + issuer, audience: POLICY_AUD, }); } catch { diff --git a/wrangler.jsonc b/wrangler.jsonc index d66f503..53a65cd 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -11,6 +11,7 @@ ], "vars": { // Production deploys must also define POLICY_AUD and TEAM_DOMAIN. + // TEAM_DOMAIN may be the base Access URL or the full /cdn-cgi/access/certs URL. // The worker now fails closed outside local development if Access is not configured. "DOMAINS": "example.com", "EMAIL_ADDRESSES": [] @@ -67,4 +68,4 @@ ] } ] -} \ No newline at end of file +}