Development -> main#2299
Conversation
Introduces cryptographic request verification on all /api/admin/* routes using Cloudflare Access JWT assertions validated against the team JWKS endpoint. When CF_ACCESS_TEAM_DOMAIN and CF_ACCESS_AUD are set, access is gated exclusively on a valid signed JWT — no password fallthrough. Also hardens the admin SPA authentication flow with improved concurrency handling, cancellation safety, and CF Access identity forwarding. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
feat: add Cloudflare Zero Trust authentication layer for admin routes
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (9)
📝 WalkthroughWalkthroughThis PR integrates Cloudflare Access authentication into the admin application. The frontend detects CF Access availability and conditionally handles authentication, while the API client and backend now support CF Access JWT verification as an alternative to bearer token authentication. Changes
Sequence DiagramsequenceDiagram
participant Client as Admin Client
participant CFE as CF Access<br/>Identity Endpoint
participant APIClient as API Client
participant Backend as Admin API
participant CFPK as CF JWKS<br/>Provider
rect rgba(100, 150, 200, 0.5)
Note over Client: App Initialization
Client->>CFE: GET /cdn-cgi/access/get-identity
CFE-->>Client: { email, name, jwt }
Client->>Client: isBehindCFAccess() = true
Client->>Client: Redirect to /dashboard
end
rect rgba(150, 100, 200, 0.5)
Note over Client: Authenticated API Request
Client->>APIClient: GET /api/admin/data
APIClient->>CFE: Retrieve JWT assertion
CFE-->>APIClient: jwt token
APIClient->>Backend: GET /api/admin/data<br/>(CF-Access-JWT-Assertion: jwt)
end
rect rgba(200, 150, 100, 0.5)
Note over Backend: JWT Verification
Backend->>CFPK: Fetch JWKS (cached)
CFPK-->>Backend: Public keys
Backend->>Backend: jwtVerify(token, keys,<br/>audience, issuer)
Backend-->>Client: 200 OK + data
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested labels
Suggested reviewers
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Coverage Report for API Unit Tests Coverage (./packages/api)
File CoverageNo changed files found. |
Coverage Report for Expo Unit Tests Coverage (./apps/expo)
File CoverageNo changed files found. |
There was a problem hiding this comment.
Pull request overview
Adds optional Cloudflare Zero Trust / Access authentication for the admin surface by verifying CF Access JWT assertions server-side and teaching the admin SPA to forward the assertion when available.
Changes:
- Add CF Access env vars and a Worker-side
verifyCFAccessRequestmiddleware that verifiesCF-Access-JWT-Assertionvia remote JWKS. - Tighten admin session JWT verification by setting/validating issuer + audience, and switch admin auth to prefer CF Access when configured.
- Add admin SPA utilities to detect CF Access, fetch
/cdn-cgi/access/get-identity, and attach the assertion to admin API requests; adjust login/auth-guard flows accordingly.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/api/wrangler.jsonc | Documents optional CF Access configuration for admin route protection. |
| packages/api/src/utils/env-validation.ts | Introduces CF Access env variables in the validated API env schema. |
| packages/api/src/routes/admin/index.ts | Implements CF Access-based admin guarding and strengthens admin JWT issuer/audience checks. |
| packages/api/src/middleware/index.ts | Re-exports CF Access types/helpers from the middleware barrel. |
| packages/api/src/middleware/cfAccess.ts | Adds CF Access JWT extraction + verification against the team JWKS. |
| apps/admin/lib/cfAccess.ts | Adds client utilities to fetch/memoize CF Access identity + JWT assertion. |
| apps/admin/lib/api.ts | Updates admin API client to send CF Access assertion header when present. |
| apps/admin/components/auth-guard.tsx | Allows access when behind CF Access, otherwise enforces existing session token behavior. |
| apps/admin/app/login/page.tsx | Auto-redirects away from login when behind CF Access; shows local-dev hint otherwise. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Cloudflare Zero Trust / Access (optional — enables CF Access JWT verification for admin routes) | ||
| CF_ACCESS_TEAM_DOMAIN: z.string().optional(), // e.g. "packrat.cloudflareaccess.com" | ||
| CF_ACCESS_AUD: z.string().optional(), // CF Access policy Application Audience tag |
There was a problem hiding this comment.
CF_ACCESS_TEAM_DOMAIN/CF_ACCESS_AUD are independently optional, which makes it easy to misconfigure production: if only one is set, the API will silently fall back to local Basic/Bearer auth. Consider validating these as an all-or-nothing pair (e.g., schema refinement requiring both or neither), and validate CF_ACCESS_TEAM_DOMAIN as a full URL (including https://) to match verifyCFAccessRequest expectations; also update the example value in the comment accordingly.
| const env = getEnv(); | ||
| const { CF_ACCESS_TEAM_DOMAIN, CF_ACCESS_AUD } = env; | ||
|
|
||
| const header = request.headers.get('authorization') ?? ''; | ||
| if (header.startsWith('Bearer ')) { | ||
| return verifyAdminJwt(header.slice(7)); | ||
| } | ||
| if (header.startsWith('Basic ')) { | ||
| return basicAuthGuard(request).authorized; | ||
| if (CF_ACCESS_TEAM_DOMAIN && CF_ACCESS_AUD) { | ||
| // CF Access configured: cryptographic JWT verification only, no fallthrough. | ||
| const cfIdentity = await verifyCFAccessRequest(request, CF_ACCESS_TEAM_DOMAIN, CF_ACCESS_AUD); | ||
| return cfIdentity !== null; | ||
| } | ||
|
|
||
| // CF Access not configured — local dev fallbacks only. | ||
| const header = request.headers.get('authorization') ?? ''; | ||
| if (header.startsWith('Bearer ')) return verifyAdminJwt(header.slice(7)); | ||
| if (header.startsWith('Basic ')) return basicAuthGuard(request).authorized; |
There was a problem hiding this comment.
adminAuthGuard treats CF Access as enabled only when both env vars are set; if one is missing (or empty), it falls back to Bearer/Basic auth. That fallback is a security footgun in production because a partial/mistyped CF Access config can unintentionally re-enable local auth methods. Prefer failing closed (deny all admin requests or throw on startup) when either CF_ACCESS_TEAM_DOMAIN or CF_ACCESS_AUD is set without the other.
| if (CF_ACCESS_TEAM_DOMAIN && CF_ACCESS_AUD) { | ||
| // CF Access configured: cryptographic JWT verification only, no fallthrough. | ||
| const cfIdentity = await verifyCFAccessRequest(request, CF_ACCESS_TEAM_DOMAIN, CF_ACCESS_AUD); | ||
| return cfIdentity !== null; |
There was a problem hiding this comment.
New CF Access auth behavior (strict mode when CF_ACCESS_TEAM_DOMAIN/CF_ACCESS_AUD are configured) isn’t covered by tests. Since packages/api/test/admin.test.ts already asserts admin auth behavior, it would be good to add cases for: (1) CF Access configured → Basic/Bearer rejected; (2) valid CF assertion header accepted (can be done by stubbing the JWKS fetch used by createRemoteJWKSet).
| // - Local development: .dev.vars file (not committed to git) | ||
| // | ||
| // Cloudflare Zero Trust / Access (optional — set to enable JWT verification on /api/admin/*): | ||
| // CF_ACCESS_TEAM_DOMAIN=<team>.cloudflareaccess.com |
There was a problem hiding this comment.
The configuration comment suggests CF_ACCESS_TEAM_DOMAIN=<team>.cloudflareaccess.com, but verifyCFAccessRequest expects teamDomain to be a full URL (e.g. https://<team>.cloudflareaccess.com). Updating this example (and keeping it consistent with apiEnvSchema) will prevent misconfiguration that would otherwise cause all admin requests to be denied.
| // CF_ACCESS_TEAM_DOMAIN=<team>.cloudflareaccess.com | |
| // CF_ACCESS_TEAM_DOMAIN=https://<team>.cloudflareaccess.com |
Description
Closes #
Type of change
Area(s) affected
apps/expo)packages/api)apps/landing)apps/guides).github/)Testing
curlor Postman)Screenshots / recordings
Pre-merge checklist
bun format && bun lintpasses with no errorsbun check-typespasses with no errorsfeat:,fix:,chore:, etc.)Summary by CodeRabbit
Release Notes