# How Cloudflare Access JWT Authentication Works in OpenSEO: Complete Technical Guide

> Learn how Cloudflare Access JWT authentication works in OpenSEO. Discover token verification, signature validation, and user context building for secure authorization.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: deep-dive
- Published: 2026-09-05

---

**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`](https://github.com/every-app/open-seo/blob/main/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:

```typescript
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:

```typescript
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:

```typescript
const { payload } = await jwtVerify(token, jwks);

```

This single call validates:
- **Signature authenticity** against the JWK set
- **Token expiration** (`exp` claim)
- **Audience restriction** (`aud` claim 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`](https://github.com/every-app/open-seo/blob/main/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:

```typescript
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`](https://github.com/every-app/open-seo/blob/main/src/lib/auth-mode.ts)** module defines three supported modes:

```typescript
// 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:

```typescript
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`](https://github.com/every-app/open-seo/blob/main/src/server/auth/workspace-merge.ts) | Ensures workspace ownership verification via Cloudflare identity |
| MCP transport initialization | [`src/server/mcp/transport.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts) | Resolves correct user context for model context protocol sessions |
| Legacy workspace functions | [`src/serverFunctions/workspace.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/workspace.ts) | Per-user workspace isolation |

## Complete Middleware Implementation Reference

The core authentication logic in [`src/middleware/ensure-user/cloudflareAccess.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensure-user/cloudflareAccess.ts) integrates all components:

```typescript
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:

```typescript
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`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensure-user/cloudflareAccess.ts) | Core middleware | JWT extraction, verification, context building |
| [`src/middleware/ensure-user/accessTokenErrors.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensure-user/accessTokenErrors.ts) | Error mapping | Translates jose verification failures to `AppError` |
| [`src/lib/auth-mode.ts`](https://github.com/every-app/open-seo/blob/main/src/lib/auth-mode.ts) | Mode definitions | `AuthMode` type, `getAuthMode()` validation |
| [`src/server/mcp/transport.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts) | MCP integration | Auth mode-aware transport initialization |
| [`src/server/auth/workspace-merge.ts`](https://github.com/every-app/open-seo/blob/main/src/server/auth/workspace-merge.ts) | Protected operation | Example of mode-guarded business logic |
| [`src/serverFunctions/workspace.ts`](https://github.com/every-app/open-seo/blob/main/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-assertion` header with cryptographic verification via the jose library
- **[`src/middleware/ensure-user/cloudflareAccess.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensure-user/cloudflareAccess.ts)** orchestrates token extraction, remote JWK resolution, and context building
- **`env.AUTH_MODE`** controls which authentication path is active; `cloudflare_access` enables 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.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensure-user/accessTokenErrors.ts) provides 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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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.