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 toresolveCloudflareAccessContext - 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
expclaim
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 JWTsubuserEmail: From JWTemailorganizationId: Derived from workspace resolutionbaseUrl: 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-assertionheader handleSelfHostedOpenSeoMcpRequestinsrc/server/mcp/transport.tsroutes requests based onauthModeconfigurationresolveCloudflareAccessContextinsrc/middleware/ensure-user/cloudflareAccess.tsperforms complete JWT verification using thejoselibrary- Environment variables
TEAM_DOMAINandPOLICY_AUDare required; missing configuration produces clearAUTH_CONFIG_MISSINGerrors - 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →