How the Open-SEO OAuth Provider Works for MCP Authentication: A Complete Technical Guide
Open-SEO implements MCP authentication through a custom OAuth 2.0 provider built on Cloudflare's @cloudflare/workers-oauth-provider library, using short-lived access tokens, mandatory scope enforcement, and dynamic client registration to secure Model-Context-Protocol endpoints.
The every-app/open-seo repository provides a self-contained OAuth 2.0 provider that authenticates MCP (Model-Context-Protocol) requests. This implementation bridges standard OAuth flows with MCP-specific requirements, ensuring every tool call originates from a properly scoped and validated client.
Core OAuth Provider Architecture
The createOpenSeoOAuthProvider() function in [src/server/mcp/oauth-provider.ts](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts) wires together three interconnected systems:
| Component | Constants/Functions | Purpose |
|---|---|---|
| OAuth endpoints | OAUTH_AUTHORIZE_PATH, OAUTH_TOKEN_PATH, OAUTH_REGISTER_PATH |
Handle Authorization Code flow, token issuance, and Dynamic Client Registration |
| Token lifecycles | MCP_ACCESS_TOKEN_TTL_SECONDS (24h), MCP_REFRESH_TOKEN_TTL_SECONDS (30d), MCP_CLIENT_REGISTRATION_TTL_SECONDS (365d) |
Balance security with usability for automated MCP clients |
| Request routing | createOpenSeoMcpServer + handleAuthenticatedOpenSeoMcpRequest |
Bridge validated tokens to actual MCP tool execution |
The provider stores authentication context in Cloudflare KV, enabling stateless validation of bearer tokens on every request.
Step-by-Step OAuth Flow for MCP
1. Authorization Request Handling
When a client initiates authentication, handleOAuthAuthorizeRequest() (lines 51-70 of the provider) processes the incoming request:
// Client redirects user to:
// /api/auth/oauth2/authorize?response_type=code&client_id=...&scope=offline_access+mcp&state=...
// The provider parses with:
const authRequest = await oauth.parseAuthRequest(request);
If the user lacks a session, they redirect to the sign-in page. Authenticated users proceed to the consent interface at /oauth-consent.
2. Consent Processing and Scope Granting
The consent endpoint (/api/oauth/consent) invokes handleOAuthConsentResponse(). This function:
- Validates CSRF protection on the submission
- Resolves the authenticated user via
resolveContextForConsent - Enforces the mandatory
mcpscope throughgetGrantedMcpScopes()(lines 26-38) - Constructs MCP authentication props with
createWorkersOAuthMcpProps()from [src/server/mcp/context.ts](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts#L55-L61)
// Scope enforcement ensures 'mcp' is always present
const grantedScopes = getGrantedMcpScopes(requestedScopes); // ['mcp', ...]
3. Token Exchange with Lifecycle Management
After consent, clients exchange the authorization code at /api/auth/oauth2/token. The tokenExchangeCallback (lines 416-431 of the provider) executes on every exchange:
// Token request (POST to /api/auth/oauth2/token)
const body = new URLSearchParams({
grant_type: "authorization_code",
code: AUTHORIZATION_CODE,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
code_verifier: PKCE_VERIFIER,
});
const response = await fetch(`${BASE_URL}/api/auth/oauth2/token`, {
method: "POST",
body,
headers: { "Content-Type": "application/x-www-form-urlencoded" },
});
// Returns: { access_token, refresh_token, expires_in, scope }
The callback verifies scope integrity and retrieves the stored workersOAuthMcpPropsSchema context for the new token pair.
4. MCP Request Authentication and Routing
Every protected MCP request passes through handleAuthenticatedOpenSeoMcpRequest() in [src/server/mcp/transport.ts](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts) (lines 45-62):
async function callMcpTool(accessToken: string) {
const response = await fetch(`${BASE_URL}/mcp`, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Mcp-Method": "whoami",
"Mcp-Name": "whoami",
},
body: JSON.stringify({
jsonrpc: "2.0",
method: "whoami",
id: 1,
}),
});
return response.json();
}
The transport layer validates three critical properties:
- Token payload conforms to
hostedWorkersOAuthMcpPropsSchema clientIdandscopesfields are present- The
mcpscope is explicitly granted
5. Token Refresh for Long-Running Clients
MCP clients with offline_access scope can obtain fresh access tokens:
async function rotateCredentials(refreshToken: string) {
const response = await fetch(`${BASE_URL}/api/auth/oauth2/token`, {
method: "POST",
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: CLIENT_ID,
}),
headers: { "Content-Type": "application/x-www-form-urlencoded" },
});
return response.json(); // New token pair with rotated refresh token
}
The refresh flow uses the same tokenExchangeCallback, ensuring consistent scope validation.
MCP-OAuth Integration Mechanisms
Resource Identification and Discovery
[src/lib/oauth-resource.ts](https://github.com/every-app/open-seo/blob/main/src/lib/oauth-resource.ts#L5-L7) defines the canonical MCP resource:
export function getMcpResource(baseUrl: string): string {
return `${baseUrl}/mcp`;
}
This URL appears in the OAuth provider's discovery document, enabling clients to locate protected endpoints dynamically.
Authentication Context Propagation
The McpProps interface (defined in [src/server/mcp/context.ts](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts)) serializes into KV storage:
| Field | Source | Usage in Tools |
|---|---|---|
userId |
Signed-in session | User-specific data filtering |
clientId |
OAuth client registration | Rate limiting and audit logging |
scopes |
Granted consent | Capability enforcement |
createMcpToolContext() extracts these properties for every tool invocation, making them available to whoami, search-console-tools, and other MCP capabilities.
Expired Data Cleanup
The purgeExpiredData() method (lines 64-71 of the provider) removes orphaned grants and tokens that KV TTLs cannot fully reclaim. A Cloudflare cron trigger invokes this routine to maintain storage efficiency.
Complete Provider Initialization Example
import { createOpenSeoOAuthProvider } from "@/server/mcp/oauth-provider";
// appFetch handles non-MCP routes (UI, API, etc.)
const appFetch = async (request: Request, env: Env, ctx: ExecutionContext) => {
return router.fetch(request, env, ctx);
};
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
const oauthProvider = createOpenSeoOAuthProvider(() => appFetch(request, env, ctx));
return oauthProvider.fetch(request, env, ctx);
},
};
This pattern allows the OAuth provider to intercept authentication routes while transparently passing other requests to the application router.
Key Implementation Files
| File | Responsibility |
|---|---|
[src/server/mcp/oauth-provider.ts](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts) |
Provider factory, endpoint handlers, token lifecycle, consent flow |
[src/server/mcp/context.ts](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts) |
McpProps definition, schema validation, tool context construction |
[src/server/mcp/transport.ts](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts) |
Token validation, CORS handling, request forwarding to MCP server |
[src/lib/oauth-resource.ts](https://github.com/every-app/open-seo/blob/main/src/lib/oauth-resource.ts) |
Resource URL construction, supported scopes registry |
[src/server/mcp/oauth-refresh.e2e.test.ts](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-refresh.e2e.test.ts) |
End-to-end token lifecycle verification |
Summary
- OAuth 2.0 foundation: Built on
@cloudflare/workers-oauth-providerwith custom MCP adaptations - Mandatory scope enforcement: Every token must include the
mcpscope; absence causes immediate rejection - Tiered token lifetimes: 24-hour access tokens, 30-day refresh tokens, 1-year client registrations
- Stateless validation: KV-stored
McpPropsenables distributed request authentication without session affinity - Complete flow coverage: Authorization, consent, token exchange, refresh, and cleanup all implemented
Frequently Asked Questions
What OAuth 2.0 grant types does the Open-SEO MCP provider support?
The provider implements Authorization Code flow with PKCE for initial authentication and Refresh Token flow for credential rotation. These grants are handled by handleOAuthAuthorizeRequest() and the tokenExchangeCallback respectively, as defined in src/server/mcp/oauth-provider.ts.
Why is the mcp scope mandatory for all tokens?
The getGrantedMcpScopes() function (lines 26-38 of the provider) explicitly injects mcp into every granted scope set. This design ensures that handleAuthenticatedOpenSeoMcpRequest() in transport.ts can definitively reject any token lacking MCP authorization, preventing scope confusion attacks.
How does the provider handle token storage and validation?
Authentication context is serialized as workersOAuthMcpPropsSchema and stored in Cloudflare KV at token creation time. Subsequent requests validate the bearer token against hostedWorkersOAuthMcpPropsSchema, which requires clientId and scopes fields. This schema-based approach provides type-safe validation without database queries on the hot path.
Can the OAuth provider be used for non-MCP authentication?
Yes. The createOpenSeoOAuthProvider() factory accepts an arbitrary appFetch handler, allowing the same OAuth infrastructure to protect UI routes, REST APIs, and other endpoints. However, the specialized TTL constants (MCP_ACCESS_TOKEN_TTL_SECONDS, etc.) and scope enforcement are optimized for automated MCP client patterns rather than interactive user sessions.
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 →