How Cloudflare Access JWT Authentication Works in OpenSEO: Complete Technical Guide
Cloudflare Access JWT authentication in OpenSEO extracts and verifies tokens via the cf-access-jwt-assertion header using the jose library, validates signatures against Cloudflare's JWK set, and builds an EnsuredUserContext for server-side authorization.
OpenSEO supports three authentication modes, one of which is cloudflare_access. This mode enables organizations to leverage Cloudflare Access as their identity provider, using industry-standard JWTs to authenticate users and authorize requests across the platform. According to the OpenSEO source code, the implementation relies on middleware-based token extraction, cryptographic verification via remote JWK sets, and explicit auth mode guards in protected server functions.
Cloudflare Access JWT Authentication Flow
The authentication process follows a strict five-step pipeline implemented in src/middleware/ensure-user/cloudflareAccess.ts. Each step handles a specific security concern, from header extraction to error classification.
Step 1: Extract the JWT from Request Headers
When Cloudflare Access protects an application, it automatically injects the cf-access-jwt-assertion header into every authenticated request. The middleware reads this header first:
const token = request.headers.get("cf-access-jwt-assertion");
if (!token) throw new AppError("Missing Cloudflare Access token");
Missing tokens trigger an immediate failure—there is no fallback to other authentication methods in cloudflare_access mode.
Step 2: Resolve and Validate the JWK Set
The middleware validates the team domain via validateTeamDomain, then constructs the well-known JWK endpoint:
const teamDomain = validateTeamDomain(env.TEAM_DOMAIN);
const jwks = createRemoteJWKSet(new URL(`${teamDomain}/.well-known/jwks.json`));
The createRemoteJWKSet function from the jose library fetches Cloudflare Access's public signing keys. These keys rotate periodically, and the remote JWK set handles caching and automatic refresh transparently.
Step 3: Cryptographic Token Verification
The core verification uses jwtVerify from jose:
const { payload } = await jwtVerify(token, jwks);
This single call validates:
- Signature authenticity against the JWK set
- Token expiration (
expclaim) - Audience restriction (
audclaim matches your Access application) - Issuer verification (tokens issued by your Cloudflare Access team domain)
Step 4: Error Classification with AppError
Verification failures map to structured errors via classifyAccessVerificationError in src/middleware/ensure-user/accessTokenErrors.ts. Common failure modes include:
| Error Condition | Mapped AppError |
|---|---|
| Missing or unreachable JWK set | Connectivity/Configuration error |
| Malformed JWT payload | Invalid token format |
| Expired token | Authentication expired |
| Invalid signature | Token tampering detected |
This classification ensures upstream handlers receive consistent, actionable error information.
Step 5: Build the EnsuredUserContext
On successful verification, the middleware constructs the context object used throughout OpenSEO:
return {
userId: payload.sub as string, // Cloudflare Access user UUID
organizationId: payload["org_id"] as string, // Delegated workspace or "cloudflare"
authMode: "cloudflare_access",
};
This EnsuredUserContext becomes the source of truth for all subsequent authorization decisions.
Auth Mode Integration and Guards
OpenSEO uses explicit auth mode checking to prevent authentication bypass. The src/lib/auth-mode.ts module defines three supported modes:
// Supported auth modes: cloudflare_access, local_noauth, hosted
export type AuthMode = "cloudflare_access" | "local_noauth" | "hosted";
export function getAuthMode(mode: string): AuthMode { ... }
Environment-Based Mode Selection
The env.AUTH_MODE environment variable determines which authentication path is active. Server functions check this before executing Cloudflare Access-specific logic:
import { env } from "cloudflare:workers";
import { getAuthMode } from "@/lib/auth-mode";
export async function workspaceMergeHandler(req: Request) {
if (getAuthMode(env.AUTH_MODE) !== "cloudflare_access") {
throw new AppError("Workspace merge is only available in cloudflare_access auth mode.");
}
// Proceed with Cloudflare Access authentication...
const ctx = await ensureCloudflareUser(req);
}
Protected Operations in OpenSEO
Several critical operations enforce cloudflare_access mode:
| Operation | Source File | Guard Purpose |
|---|---|---|
| Workspace merge | src/server/auth/workspace-merge.ts |
Ensures workspace ownership verification via Cloudflare identity |
| MCP transport initialization | src/server/mcp/transport.ts |
Resolves correct user context for model context protocol sessions |
| Legacy workspace functions | src/serverFunctions/workspace.ts |
Per-user workspace isolation |
Complete Middleware Implementation Reference
The core authentication logic in src/middleware/ensure-user/cloudflareAccess.ts integrates all components:
import { env } from "cloudflare:workers";
import { createRemoteJWKSet, jwtVerify } from "jose";
import { AppError } from "@/server/lib/errors";
import { validateTeamDomain } from "@/shared/selfhost-checks";
export async function ensureCloudflareUser(request: Request) {
// Extract token from Cloudflare Access header
const token = request.headers.get("cf-access-jwt-assertion");
if (!token) throw new AppError("Missing Cloudflare Access token");
// Resolve team domain and remote JWK set
const teamDomain = validateTeamDomain(env.TEAM_DOMAIN);
const jwks = createRemoteJWKSet(new URL(`${teamDomain}/.well-known/jwks.json`));
// Verify and parse the JWT
try {
const { payload } = await jwtVerify(token, jwks);
return {
userId: payload.sub as string,
organizationId: payload["org_id"] as string,
authMode: "cloudflare_access",
};
} catch (e) {
// Map jose errors to application-specific errors
throw classifyAccessVerificationError(e);
}
}
Dependency: JWK Set Fetching
The remote JWK set mechanism eliminates manual key management:
const jwks = createRemoteJWKSet(new URL(`${teamDomain}/.well-known/jwks.json`));
Cloudflare Access rotates signing keys regularly. The jose library handles HTTP caching headers, key ID (kid) matching, and automatic refresh when unknown key IDs appear.
Key Source Files and Responsibilities
| File Path | Component | Responsibility |
|---|---|---|
src/middleware/ensure-user/cloudflareAccess.ts |
Core middleware | JWT extraction, verification, context building |
src/middleware/ensure-user/accessTokenErrors.ts |
Error mapping | Translates jose verification failures to AppError |
src/lib/auth-mode.ts |
Mode definitions | AuthMode type, getAuthMode() validation |
src/server/mcp/transport.ts |
MCP integration | Auth mode-aware transport initialization |
src/server/auth/workspace-merge.ts |
Protected operation | Example of mode-guarded business logic |
src/serverFunctions/workspace.ts |
Legacy guards | Per-user workspace authorization |
Security Considerations
Token binding to request context — The cf-access-jwt-assertion header is injected by Cloudflare's edge network and cannot be forged by end users. Your origin server must run behind Cloudflare Access for this security property to hold.
Team domain validation — The validateTeamDomain function ensures only properly formatted Cloudflare Access domains are used, preventing JWK set redirection attacks.
No local fallback — Unlike local_noauth mode, cloudflare_access mode has no bypass. Missing or invalid tokens always reject the request.
Summary
- Cloudflare Access JWT authentication in OpenSEO uses the
cf-access-jwt-assertionheader with cryptographic verification via the jose library src/middleware/ensure-user/cloudflareAccess.tsorchestrates token extraction, remote JWK resolution, and context buildingenv.AUTH_MODEcontrols which authentication path is active;cloudflare_accessenables the full JWT verification pipeline- Protected operations explicitly check
getAuthMode(env.AUTH_MODE)before executing Cloudflare Access-specific logic - Error classification in
src/middleware/ensure-user/accessTokenErrors.tsprovides structured failure handling for debugging and user feedback
Frequently Asked Questions
What header does Cloudflare Access use for JWT authentication?
Cloudflare Access adds the cf-access-jwt-assertion header to every authenticated request. OpenSEO's middleware specifically looks for this header in src/middleware/ensure-user/cloudflareAccess.ts. Missing headers trigger an immediate AppError with message "Missing Cloudflare Access token".
How does OpenSEO verify the Cloudflare Access JWT signature?
OpenSEO uses createRemoteJWKSet from the jose library to fetch public signing keys from <team-domain>/.well-known/jwks.json. The jwtVerify function then validates the token signature, expiration, audience, and issuer against these keys. This approach handles automatic key rotation without configuration changes.
Can I use Cloudflare Access authentication in local development?
The cloudflare_access mode requires env.TEAM_DOMAIN to be set to a valid Cloudflare Access team domain with reachable JWK endpoints. For local development without Cloudflare Access, OpenSEO provides local_noauth mode defined in src/lib/auth-mode.ts. The hosted mode offers a third alternative for managed deployments.
What happens when JWT verification fails in OpenSEO?
Verification failures route through classifyAccessVerificationError in src/middleware/ensure-user/accessTokenErrors.ts. This function maps jose-specific errors (JWS verification failed, JWT expired, JWT malformed) to structured AppError instances that upstream handlers can catch and respond to appropriately.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →