How Self-Hosted MCP Authentication Works with Cloudflare Access in Open SEO

Self-hosted MCP authentication with Cloudflare Access uses JWT validation on the cf-access-jwt-assertion header, mapping verified users to a shared workspace without session storage.

Open SEO's self-hosted deployment option lets you protect your Model-Context-Protocol (MCP) endpoint using Cloudflare Access, a Zero Trust security platform. This authentication flow is stateless, relying entirely on cryptographically signed JWTs rather than server-side sessions. The implementation spans two primary modules: handleSelfHostedOpenSeoMcpRequest in src/server/mcp/transport.ts and resolveCloudflareAccessContext in src/middleware/ensure-user/cloudflareAccess.ts.

Core Authentication Modules

The self-hosted MCP authentication architecture centers on two functions that handle request ingress and identity verification.

Entry Point: handleSelfHostedOpenSeoMcpRequest

Located in src/server/mcp/transport.ts at lines 64-78, this function serves as the gatekeeper for all self-hosted MCP traffic. It inspects the authMode configuration parameter and routes requests to the appropriate resolver.

  • If authMode === "cloudflare_access", it delegates to resolveCloudflareAccessContext
  • If authMode === "local_noauth", it uses a local no-authentication resolver for development

This branching logic ensures production deployments enforce Cloudflare Access while allowing unauthenticated local testing.

JWT Validation: resolveCloudflareAccessContext

The resolveCloudflareAccessContext function in src/middleware/ensure-user/cloudflareAccess.ts (lines 39-97) performs the cryptographic validation of Cloudflare-issued tokens. It implements the complete OAuth 2.0/JWT verification pattern without external session dependencies.

Step-by-Step Authentication Flow

Understanding how self-hosted MCP authentication works requires tracing ten discrete operations from request arrival to MCP dispatch:

1. Cloudflare Access Injects the JWT

Cloudflare's edge network adds a signed JWT to every authenticated request via the cf-access-jwt-assertion header. This token contains claims about the authenticated user and is signed by Cloudflare's private keys.

2. Authentication Mode Selection

The handleSelfHostedOpenSeoMcpRequest function examines authMode and calls resolveCloudflareAccessContext when Cloudflare Access protection is configured (lines 75-78 in transport.ts).

3. Configuration Validation

Before processing tokens, the resolver verifies that two environment variables exist:

  • TEAM_DOMAIN – your Cloudflare Access team domain (e.g., your-team.cloudflareaccess.com)
  • POLICY_AUD – the JWT audience claim matching your Cloudflare Access application

Missing configuration triggers an AppError with code AUTH_CONFIG_MISSING, halting the request with a clear operator-facing message (lines 42-58).

4. JWT Extraction

The function reads cf-access-jwt-assertion from request headers. Absence indicates Cloudflare Access is not properly configured in front of the endpoint, resulting in another AUTH_CONFIG_MISSING error (lines 60-70).

5. JWK Set Retrieval

The getJwks helper (lines 14-27) fetches the JSON Web Key set from:


https://<TEAM_DOMAIN>/cdn-cgi/access/certs

This public endpoint publishes Cloudflare's current signing keys. Results are cached per-team-domain to minimize latency and external dependencies.

6. Cryptographic Verification

Using the jose library's jwtVerify function, the resolver validates:

  • Signature: Against the fetched JWK set
  • Issuer: Matches TEAM_DOMAIN
  • Audience: Matches POLICY_AUD
  • Expiration: Standard JWT exp claim

Verification failures are classified and re-thrown as user-friendly AppError instances (lines 75-88).

7. User Claim Extraction

Upon successful verification, the payload's sub (user ID) and email claims are extracted. Missing either claim produces an UNAUTHENTICATED error, as Open SEO requires both identifiers for workspace resolution (lines 90-96).

8. Workspace Resolution

The extracted identity is passed to resolveSharedWorkspaceContext (line 97), which maps all Cloudflare Access users to a single shared workspace model. This design simplifies multi-tenant handling in self-hosted environments by treating all authenticated users as members of one organizational unit.

9. MCP Context Construction

createWorkersOAuthMcpProps assembles the final context object containing:

  • userId: From JWT sub
  • userEmail: From JWT email
  • organizationId: Derived from workspace resolution
  • baseUrl: Public origin of the request

This schema is defined by hostedWorkersOAuthMcpPropsSchema in src/server/mcp/context.ts.

10. Request Dispatch

Finally, createRequestHandler applies CORS headers, legacy request normalization, and invokes the Agents SDK handler to process the MCP method call (lines 86-87 and 125-129).

Environment Configuration

Deploying self-hosted MCP authentication with Cloudflare Access requires specific environment variables:

Variable Purpose Example
AUTH_MODE Selects authentication strategy cloudflare_access
TEAM_DOMAIN Cloudflare Access team domain mycompany.cloudflareaccess.com
POLICY_AUD Application audience from Cloudflare a1b2c3d4e5f678901234567890123456

Code Examples

Calling a Protected MCP Endpoint

When Cloudflare Access protects your self-hosted instance, clients must include the JWT from their Cloudflare session:

curl -X POST "https://my-selfhosted.example.com/mcp" \
  -H "cf-access-jwt-assertion: <JWT-from-Cloudflare-Access>" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"whoami","params":{},"id":1}'

The JWT is typically obtained by completing the Cloudflare Access flow in a browser or using service token authentication for programmatic access.

Internal SDK Usage

The same context building pattern used by the transport layer can be invoked directly:

import { createOpenSeoMcpServer } from "@/server/mcp/server";
import { createWorkersOAuthMcpProps } from "@/server/mcp/context";

const props = createWorkersOAuthMcpProps({
  userId: "user-123",
  userEmail: "user@example.com",
  organizationId: "org-456",
  baseUrl: "https://my-selfhosted.example.com",
});

const server = createOpenSeoMcpServer(props);
await server.connect(transport);
// Handle MCP request...

Key Implementation Files

File Lines Responsibility
src/server/mcp/transport.ts 64-78, 125-129 Request routing, auth mode selection, final dispatch
src/middleware/ensure-user/cloudflareAccess.ts 14-27, 39-97 JWT validation, JWK fetching, workspace resolution
src/server/mcp/context.ts Schema + factory MCP context schema and construction
src/server/mcp/public-origin.ts Full file Public URL determination for self-hosted origins
src/server/mcp/server.ts Full file Core MCP server instantiation

Architectural Benefits

Zero Trust Security

Cloudflare Access authentication enforces identity verification at the network edge before requests reach your infrastructure. Every MCP call carries cryptographic proof of user identity without requiring server-side session state.

Stateless Operation

JWT validation eliminates database dependencies for authentication. The resolveCloudflareAccessContext function completes entirely within the request lifecycle using only environment configuration and Cloudflare's public JWK endpoint.

Simplified Multi-Tenancy

The resolveSharedWorkspaceContext design collapses all authenticated users into a single workspace, avoiding complex tenant isolation logic for self-hosted deployments where organizational boundaries are typically simpler than SaaS offerings.

Summary

  • Self-hosted MCP authentication in Open SEO uses Cloudflare Access JWT validation via the cf-access-jwt-assertion header
  • handleSelfHostedOpenSeoMcpRequest in src/server/mcp/transport.ts routes requests based on authMode configuration
  • resolveCloudflareAccessContext in src/middleware/ensure-user/cloudflareAccess.ts performs complete JWT verification using the jose library
  • Environment variables TEAM_DOMAIN and POLICY_AUD are required; missing configuration produces clear AUTH_CONFIG_MISSING errors
  • JWK caching per-team-domain optimizes repeated validation without compromising security
  • Shared workspace model simplifies self-hosted multi-tenancy by mapping all authenticated users to one organizational context

Frequently Asked Questions

What happens if the cf-access-jwt-assertion header is missing?

Open SEO returns an AUTH_CONFIG_MISSING error indicating that Cloudflare Access is not properly protecting the route. This typically means the request bypassed Cloudflare's edge network or Cloudflare Access is not configured for this application.

Can I use self-hosted MCP authentication without Cloudflare Access?

Yes. Setting AUTH_MODE=local_noauth disables JWT validation and uses a local no-authentication resolver. This configuration is intended for development environments only and should never be used in production.

How does the JWK caching work?

The getJwks helper maintains an in-memory cache of JWK sets keyed by TEAM_DOMAIN. Cached entries persist for the lifetime of the worker process, eliminating redundant HTTPS calls to Cloudflare's certificate endpoint while remaining compatible with Cloudflare's key rotation practices.

Where do I find my POLICY_AUD value?

The POLICY_AUD is the Application Audience (AUD) tag from your Cloudflare Access application configuration. In the Cloudflare dashboard, navigate to Zero Trust → Access → Applications, select your application, and copy the AUD tag from the application overview page.

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 →