How Cloudflare Access JWT Authentication Works in OpenSEO

OpenSEO validates Cloudflare Access tokens by extracting the cf-access-jwt-assertion header and verifying it against Cloudflare's rotating JWKS endpoint using the jose library in src/middleware/ensure-user/cloudflareAccess.ts.

The OpenSEO repository (every-app/open-seo) implements identity-aware authentication through Cloudflare Access JWT, enabling zero-trust security for self-hosted deployments. When the AUTH_MODE environment variable is set to cloudflare_access, the application enforces strict JWT validation on every protected API route without requiring additional application-level authentication code.

Authentication Architecture Overview

OpenSEO supports three distinct authentication modes configured via the AUTH_MODE environment variable. The system defaults to cloudflare_access for public-facing deployments, leveraging Cloudflare's reverse proxy to handle identity provider (IdP) integration upstream.

Supported Authentication Modes

  • cloudflare_access: Validates JWT tokens issued by Cloudflare Access. This mode requires the TEAM_DOMAIN and POLICY_AUD environment variables.
  • local_noauth: Disables authentication entirely, intended strictly for local development environments.
  • hosted: Delegates authentication to Better Auth, suitable for managed hosting scenarios where Cloudflare Access is unavailable.

When AUTH_MODE=cloudflare_access, the middleware chain in src/middleware/ensure-user/cloudflareAccess.ts intercepts all incoming requests before they reach server functions like those defined in src/serverFunctions/workspace.ts.

JWT Verification Implementation

The verification logic follows the OpenID Connect (OIDC) pattern, using the jose library to handle cryptographic validation against Cloudflare's public key infrastructure.

Extracting the Access Token

The middleware first extracts the JWT from the incoming request headers:

// src/middleware/ensure-user/cloudflareAccess.ts
const token = req.headers.get("cf-access-jwt-assertion");
if (!token) {
  throw new Error("Missing Cloudflare Access token");
}

Cloudflare Access sends the token via the cf-access-jwt-assertion header by default, though the middleware can also parse it from cookies when necessary.

Resolving the JWKS Endpoint

Using the TEAM_DOMAIN environment variable, the middleware constructs the JSON Web Key Set (JWKS) URL and creates a remote key set resolver:

import { createRemoteJWKSet } from "jose";

const jwksUrl = new URL(`${env.TEAM_DOMAIN}/cdn-cgi/access/certs`);
const remoteJwks = createRemoteJWKSet(jwksUrl);

This endpoint (https://<your-team>.cloudflareaccess.com/cdn-cgi/access/certs) returns the rotating public keys used to sign all Access tokens for your organization.

Verifying Token Claims

The middleware calls jwtVerify to validate the signature, expiration (exp), audience (aud), and issuer (iss) claims:

import { jwtVerify, type JWTPayload } from "jose";

const { payload } = await jwtVerify(token, remoteJwks, {
  audience: env.POLICY_AUD,
  issuer: env.TEAM_DOMAIN,
});

// Attach verified payload to request for downstream use
(req as any).user = payload as JWTPayload;

The POLICY_AUD value must match the Audience tag configured in your Cloudflare Access application policy. Failed verification immediately rejects the request before it reaches business logic in files like src/serverFunctions/workspace.ts.

Error Handling and Type Safety

OpenSEO centralizes error handling to provide actionable feedback when JWT validation fails.

Mapping Verification Errors

The src/middleware/ensure-user/accessTokenErrors.ts file imports error classes from jose and maps them to user-friendly messages:

  • JWTExpired: Returned when the token's exp claim has passed.
  • JWTClaimValidationFailed: Triggered when audience or issuer claims do not match the configured environment variables.

This separation of concerns keeps the main verification logic in cloudflareAccess.ts clean while ensuring consistent error responses across the API surface.

Type Definitions

Strong typing for the authenticated user context is defined in src/middleware/ensure-user/types.ts, which exports the JWTPayload interface used when attaching identity information to incoming requests.

Configuration and Deployment

Enabling Cloudflare Access JWT authentication requires specific environment configuration before deployment.

Required Environment Variables

Configure your production environment according to the template in .env.example:

AUTH_MODE=cloudflare_access
TEAM_DOMAIN=https://myteam.cloudflareaccess.com
POLICY_AUD=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxx

The TEAM_DOMAIN must include the protocol (https://) and match your Cloudflare Access organization URL exactly. The POLICY_AUD corresponds to the Application Audience (AUD) tag found in your Cloudflare Access dashboard under the specific application's configuration.

Middleware Activation

Once configured, the middleware automatically applies to all API routes. No manual route protection is required, as the validation occurs in the request lifecycle before server functions execute.

Consuming Authentication in Server Functions

Downstream code accesses the verified identity through the request object populated by the middleware.

// src/serverFunctions/workspace.ts
export async function getWorkspace(req: Request) {
  const user = (req as any).user;
  if (!user?.email) {
    throw new Error("Unauthenticated");
  }
  
  // Proceed with workspace-scoped logic using user.email
  return { workspaceId: user.custom?.workspace_id };
}

The JWTPayload includes standard JWT claims plus Cloudflare-specific fields like email and custom claim data configured in your Access policies.

Summary

  • OpenSEO uses src/middleware/ensure-user/cloudflareAccess.ts to validate Cloudflare Access JWT tokens on every request when AUTH_MODE=cloudflare_access.
  • The middleware extracts the cf-access-jwt-assertion header and verifies it against the JWKS endpoint at https://<TEAM_DOMAIN>/cdn-cgi/access/certs using jose.createRemoteJWKSet.
  • Validation enforces the POLICY_AUD audience claim and TEAM_DOMAIN issuer claim to prevent token replay attacks.
  • Errors are standardized in src/middleware/ensure-user/accessTokenErrors.ts, handling specific jose exceptions like JWTExpired and JWTClaimValidationFailed.
  • Verified payloads are attached to the request object for use in server functions such as src/serverFunctions/workspace.ts.

Frequently Asked Questions

What header carries the Cloudflare Access JWT in OpenSEO?

Cloudflare Access transmits the JWT in the cf-access-jwt-assertion HTTP header. The OpenSEO middleware specifically checks this header (with fallback support for cookies) in src/middleware/ensure-user/cloudflareAccess.ts before proceeding with cryptographic verification.

How does OpenSEO handle expired or invalid tokens?

When jose.jwtVerify throws JWTExpired or JWTClaimValidationFailed errors, the middleware catches these exceptions and transforms them using the mapping logic in src/middleware/ensure-user/accessTokenErrors.ts. This converts library-specific errors into standardized API responses indicating whether the token expired or failed claim validation.

Can I run OpenSEO without Cloudflare Access?

Yes. Set AUTH_MODE=local_noauth for development environments to bypass all authentication checks, or use AUTH_MODE=hosted to integrate with Better Auth instead. However, production deployments behind Cloudflare should use cloudflare_access to leverage zero-trust security at the edge.

Where are the TypeScript types for the JWT payload defined?

The JWTPayload interface and related authentication types are defined in src/middleware/ensure-user/types.ts. These types ensure type safety when accessing user identity information in server functions like src/serverFunctions/workspace.ts.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →