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

> Learn how self-hosted MCP authentication integrates with Cloudflare Access using JWT validation for secure user mapping to shared workspaces without session storage.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: internals
- Published: 2026-08-18

---

**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`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts) and `resolveCloudflareAccessContext` in [`src/middleware/ensure-user/cloudflareAccess.ts`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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:

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

```typescript
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`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts) | 64-78, 125-129 | Request routing, auth mode selection, final dispatch |
| [`src/middleware/ensure-user/cloudflareAccess.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensure-user/cloudflareAccess.ts) | 14-27, 39-97 | JWT validation, JWK fetching, workspace resolution |
| [`src/server/mcp/context.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts) | Schema + factory | MCP context schema and construction |
| [`src/server/mcp/public-origin.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/public-origin.ts) | Full file | Public URL determination for self-hosted origins |
| [`src/server/mcp/server.ts`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts) routes requests based on `authMode` configuration
- **`resolveCloudflareAccessContext`** in [`src/middleware/ensure-user/cloudflareAccess.ts`](https://github.com/every-app/open-seo/blob/main/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.