AuthCloudflare Access Integration

Cloudflare Access Integration

Cloudflare Access (part of Cloudflare Zero Trust) provides network-level authentication for admin endpoints. It acts as a defense-in-depth layer — admin...

Cloudflare Access Integration

Cloudflare Access (part of Cloudflare Zero Trust) provides network-level authentication for admin endpoints. It acts as a defense-in-depth layer — admin routes require both a valid X-Admin-Key header AND a verified Cloudflare Access JWT.

Note: Cloudflare Access protects admin routes only. Regular API authentication is handled by Clerk JWTs and API keys.


Table of Contents


Architecture

Defense-in-Depth Layers

Admin routes (/admin/storage/*) are protected by two independent layers:

LayerMechanismHeaderPurpose
1Admin KeyX-Admin-KeyApplication-level secret — constant-time comparison
2CF Access JWTCF-Access-JWT-AssertionNetwork-level identity — JWT signature verification

Both layers must pass for the request to proceed. An attacker who compromises one secret still cannot access admin endpoints.


How It Works

When a request reaches an admin endpoint:

  1. verifyAdminAuth() checks the X-Admin-Key header against env.ADMIN_KEY using constant-time comparison (timingSafeCompareWorker)

  2. verifyCfAccessJwt() verifies the CF-Access-JWT-Assertion header:

    • Fetches the JWKS from https://<team>.cloudflareaccess.com/cdn-cgi/access/certs
    • Verifies signature, audience (CF_ACCESS_AUD), issuer, and expiration using jose
    • Extracts email and sub claims from the payload
  3. If both pass, the request reaches the admin handler

JWT Verification Flow


Setting Up Cloudflare Access

Step 1: Enable Cloudflare Zero Trust

  1. Go to Cloudflare Zero Trust

  2. If not already set up, create your team name (e.g., mycompany)

    • This becomes your team domain: mycompany.cloudflareaccess.com
    • Choose carefully — this is used in JWT verification
  3. You need at least the Free Zero Trust plan (50 users)

Step 2: Create a Self-Hosted Application

  1. In Zero Trust dashboard, go to Access → Applications
  2. Click Add an application → Self-hosted
  3. Configure the application:
FieldValueNotes
Application nameBloqr Compiler AdminDisplay name
Session Duration24hHow long before re-auth
Application domainapi.bloqr.devYour Worker domain
Path/admin/*Restrict to admin routes only

Important: Set the path to /admin/* so that only admin endpoints are behind Access. Regular API and frontend routes should NOT go through Access.

  1. Click Next to configure policies

Step 3: Configure Access Policies

Create at least one Allow policy:

Policy: Admin Team (Email-based)

FieldValue
Policy nameAdmin Email Allow
ActionAllow
Include ruleEmails — admin@yourdomain.com

Alternative: Identity Provider

If you use an IdP (Google Workspace, Okta, GitHub, etc.):

FieldValue
Policy nameGitHub Org Members
ActionAllow
Include ruleLogin Methods — GitHub
Require ruleGitHub Organization — your-org

Adding Multiple Admins

You can combine multiple rules in a single policy:

  • Emails: admin1@example.com, admin2@example.com
  • Email domain: @yourdomain.com (allows entire domain)
  • GitHub org: Requires membership in a specific GitHub organization
  • IP ranges: Allow from specific IPs (e.g., office network)

Step 4: Get the Application AUD Tag

After creating the application:

  1. Go to Access → Applications

  2. Click on your Bloqr Compiler Admin application

  3. Find the Application Audience (AUD) Tag in the application overview

    • It looks like: 4a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d
  4. Copy this value — you'll need it for the Worker secret

Step 5: Configure Worker Secrets

Set the two required secrets using Wrangler:

# Set the team domain (just the subdomain, not the full URL)
wrangler secret put CF_ACCESS_TEAM_DOMAIN
# Enter: mycompany

# Set the application audience tag
wrangler secret put CF_ACCESS_AUD
# Enter: 4a1b2c3d...  (the full AUD tag from Step 4)

For local development with .dev.vars:

# .dev.vars (never commit this file)
CF_ACCESS_TEAM_DOMAIN=mycompany
CF_ACCESS_AUD=4a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d

Service Tokens for CI/CD

Cloudflare Access supports Service Tokens for non-interactive access (CI/CD pipelines, cron jobs, monitoring).

Creating a Service Token

  1. In Zero Trust dashboard, go to Access → Service Auth → Service Tokens
  2. Click Create Service Token
  3. Name it (e.g., CI/CD Pipeline)
  4. Copy the Client ID and Client Secret — the secret is shown only once

Using Service Tokens in CI/CD

# GitHub Actions example
curl -X POST "https://api.bloqr.dev/admin/storage/stats" \
  -H "X-Admin-Key: ${{ secrets.ADMIN_KEY }}" \
  -H "CF-Access-Client-Id: ${{ secrets.CF_ACCESS_CLIENT_ID }}" \
  -H "CF-Access-Client-Secret: ${{ secrets.CF_ACCESS_CLIENT_SECRET }}"

Adding Service Token Policy

Add a Service Auth policy to your Access application:

FieldValue
Policy nameCI/CD Service Token
ActionService Auth
Include ruleService Token — CI/CD Pipeline

Local Development

When CF_ACCESS_TEAM_DOMAIN or CF_ACCESS_AUD are not set, the CF Access middleware gracefully skips verification and returns { valid: true }. This means:

  • Local dev (wrangler dev): Works without CF Access — only X-Admin-Key is required for admin routes
  • Production: Both layers are enforced when both secrets are configured

Testing CF Access Locally

If you need to test CF Access locally:

  1. Set CF_ACCESS_TEAM_DOMAIN and CF_ACCESS_AUD in .dev.vars

  2. Obtain a valid CF Access JWT:

    • Visit your Access-protected URL in a browser
    • After authenticating, the CF_Authorization cookie contains the JWT
    • Copy the JWT value
  3. Include it in requests:

curl -X GET "http://localhost:8787/admin/storage/stats" \
  -H "X-Admin-Key: your-admin-key" \
  -H "CF-Access-JWT-Assertion: eyJhbGciOiJS..."

Integration with Clerk Auth

CF Access and Clerk serve different purposes in the auth stack:

AspectClerkCloudflare Access
ScopeAll API routesAdmin routes only
UsersAPI consumers, frontend usersAdministrators, CI/CD
IdentityClerk user ID, email, tierCF email, identity
KeysJWT + API keysJWT (auto-injected by Access proxy)
ManagementClerk DashboardCF Zero Trust Dashboard

How They Coexist

Regular API request:
  User → Clerk JWT/API Key → authenticateRequestUnified() → Handler

Admin request (browser):
  Admin → CF Access (injects JWT) → verifyAdminAuth() → verifyCfAccessJwt() → Handler

Admin request (CI/CD):
  Pipeline → CF Service Token → verifyAdminAuth() → verifyCfAccessJwt() → Handler

Future: Clerk-Based Admin Auth

The admin access system will eventually migrate from X-Admin-Key to Clerk tier-based authorization:

Future admin request:
  Admin → CF Access → authenticateRequestUnified() → requireTier(Admin) → Handler

In this model, CF Access remains as the network-level gate, while Clerk handles identity and tier verification. See Admin Access for the migration plan.


Technical Implementation

Source File

worker/middleware/cf-access.ts

Key Types

interface CfAccessVerificationResult {
    valid: boolean;
    email?: string;
    identity?: string;
    error?: string;
}

Environment Variables

// In worker/types.ts → Env interface
CF_ACCESS_TEAM_DOMAIN?: string;  // e.g., 'mycompany'
CF_ACCESS_AUD?: string;          // Application audience tag

JWKS Caching

The JWKS resolver is cached at module level per Worker isolate:

const cfAccessJwksCache = new Map<string, JWTVerifyGetKey>();
  • Cached per certsUrl (derived from team domain)
  • Persists for the lifetime of the Worker isolate
  • Automatically refreshes when the isolate is recycled
  • Uses the same jose library as Clerk JWT verification

Usage in Worker

// worker/worker.ts — admin route protection
if (pathname.startsWith('/admin/storage/')) {
    const adminAuth = verifyAdminAuth(request, env);
    if (!adminAuth.valid) {
        return JsonResponse.unauthorized(adminAuth.error);
    }

    const cfAccess = await verifyCfAccessJwt(request, env);
    if (!cfAccess.valid) {
        return JsonResponse.forbidden(cfAccess.error);
    }

    // Both layers passed — proceed to handler
}

Troubleshooting

"Missing CF-Access-JWT-Assertion header"

Cause: Request reached the Worker without passing through CF Access.

Fix:
  • Verify the Access application path matches your admin route (/admin/*)
  • Ensure you're accessing the Worker through its public domain (not directly via wrangler dev when Access is required)
  • For CI/CD, use Service Token headers instead

"CF Access JWT verification failed: JWTExpired"

Cause: The Access session has expired.

Fix:
  • Re-authenticate through the CF Access login page
  • Increase the session duration in the Access application settings
  • For Service Tokens, tokens don't expire unless revoked

"CF Access JWT verification failed: JWSSignatureVerificationFailed"

Cause: JWT signature doesn't match the JWKS keys.

Fix:
  • Verify CF_ACCESS_TEAM_DOMAIN matches your Zero Trust team name exactly
  • Verify CF_ACCESS_AUD matches the application's audience tag exactly
  • The JWKS cache may be stale — the Worker isolate will eventually recycle

Admin route works locally but fails in production

Cause: CF Access is not configured or secrets are missing.

Fix:
# Verify secrets are set
wrangler secret list

# Re-set if needed
wrangler secret put CF_ACCESS_TEAM_DOMAIN
wrangler secret put CF_ACCESS_AUD

CF Access blocks non-admin routes

Cause: The Access application path is too broad.

Fix:
  • In Zero Trust dashboard, edit the application
  • Ensure the path is set to /admin/* (not /* or /)
  • Only admin routes should be behind Access

Further Reading