# How the Open-SEO OAuth Provider Works for MCP Authentication: A Complete Technical Guide

> Discover how the Open-SEO OAuth provider secures MCP authentication using Cloudflare Workers. Learn about short-lived tokens, scope enforcement, and dynamic client registration for Model-Context-Protocol endpoints.

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

---

**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)](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:

```typescript
// 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 `mcp` scope through `getGrantedMcpScopes()` (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)](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts#L55-L61)

```typescript
// 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:

```typescript
// 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)](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts) (lines 45-62):

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

1. Token payload conforms to `hostedWorkersOAuthMcpPropsSchema`
2. `clientId` and `scopes` fields are present
3. The `mcp` scope is explicitly granted

### 5. Token Refresh for Long-Running Clients

MCP clients with `offline_access` scope can obtain fresh access tokens:

```typescript
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)](https://github.com/every-app/open-seo/blob/main/src/lib/oauth-resource.ts#L5-L7) defines the canonical MCP resource:

```typescript
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)](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

```typescript
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)](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)](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)](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)](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)](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-provider` with custom MCP adaptations
- **Mandatory scope enforcement**: Every token must include the `mcp` scope; absence causes immediate rejection
- **Tiered token lifetimes**: 24-hour access tokens, 30-day refresh tokens, 1-year client registrations
- **Stateless validation**: KV-stored `McpProps` enables 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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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.