# How the OpenSEO OAuth Provider Handles MCP Client Registrations and Tokens

> Learn how the OpenSEO OAuth provider manages MCP client registrations and tokens using Cloudflare KV. Discover normalized requests, mandatory scopes, and token lifecycles.

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

---

**The OpenSEO OAuth provider delegates core OAuth 2.0 logic to the `@cloudflare/workers-oauth-provider` package while implementing a thin compatibility layer in [`src/server/mcp/oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts) that normalizes dynamic client registration requests, enforces mandatory `mcp` scopes, and manages token lifecycles with 24-hour access tokens and 30-day refresh tokens backed by Cloudflare KV.**

The OpenSEO application bundles a custom OAuth 2.0 provider specifically engineered for Managed Cloud Platform (MCP) integrations. According to the source code in the `every-app/open-seo` repository, this implementation exposes standard OAuth endpoints under `/api/auth/oauth2/`, handles dynamic client registration (DCR) with specialized normalization for public versus confidential clients, and persists MCP authentication context in KV storage to maintain user organization associations across token exchanges.

## OAuth Endpoint Architecture

The provider exposes three standard OAuth 2.0 endpoints defined as constants in [`src/server/mcp/oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts) at lines 32‑35:

- **`/api/auth/oauth2/authorize`** – Initiates the authorization flow
- **`/api/auth/oauth2/token`** – Handles token exchange and refresh
- **`/api/auth/oauth2/register`** – Processes dynamic client registration

These paths are wired into the `OAuthProvider` instance created by the `createProvider` factory function (lines 99‑135). The configuration limits available scopes to the MCP-specific set (`MCP_OAUTH_SCOPES`) and mandates the `mcp` scope for all operations (lines 26‑38).

## Dynamic Client Registration Normalization

When handling DCR requests, the provider applies a compatibility shim to handle variations in client authentication methods. The `normalizeClientRegistrationRequest` function in [`src/server/mcp/oauth-registration.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-registration.ts) (lines 14‑68) performs critical transformations:

1. **Validates request size** and parses the JSON payload
2. **Detects public clients** – Requests omitting `token_endpoint_auth_method` are automatically assigned `token_endpoint_auth_method: "none"`
3. **Handles confidential clients** – Special cases like Perplexity that require secrets receive `client_secret_post` authentication

This normalization ensures compatibility with diverse MCP clients before delegating to the underlying `@cloudflare/workers-oauth-provider` package.

## MCP-Specific Token Lifecycle Management

Token issuance and validation are governed by the `tokenExchangeCallback` defined in [`src/server/mcp/oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts) (lines 16‑22). This callback enforces strict MCP requirements:

```typescript
tokenExchangeCallback: ({ props, requestedScope }) => {
  if (!requestedScope.includes(MCP_SCOPE)) {
    throw new OAuthError("invalid_scope", { 
      description: "The mcp scope is required" 
    });
  }
  const authContext = workersOAuthMcpPropsSchema.parse(props)[MCP_AUTH_CONTEXT_PROP];
  return {
    accessTokenProps: createWorkersOAuthMcpProps({
      ...authContext,
      scopes: requestedScope,
    }),
  };
}

```

**Token lifetimes** are configured in the provider options (lines 48‑50):
- **Access tokens**: 24 hours
- **Refresh tokens**: 30 days
- **Client registrations**: 1 year TTL (line 55)

The callback validates that every token request includes the mandatory `mcp` scope, then reconstructs the `accessTokenProps` using the stored MCP context from KV storage, ensuring tokens remain bound to the original user ID, email, and organization.

## Authorization Flow and Context Persistence

After user consent, the `handleOAuthConsentResponse` function (lines 36‑44) creates MCP-specific properties using `createWorkersOAuthMcpProps`. These properties embed the user's authentication context and granted scopes into the KV payload. The provider then calls `oauth.completeAuthorization` (lines 45‑55) to finalize the flow, storing the context for subsequent `tokenExchangeCallback` invocations.

## Garbage Collection of Expired Registrations

The exported provider object includes a `purgeExpiredData` method (lines 64‑71) designed for cron-driven cleanup. This routine calls the underlying provider's garbage collection logic with a batch size of 200 keys, removing stale KV entries from expired client registrations and tokens without manual intervention.

## Implementing MCP OAuth Flows

### Registering a New Client

To dynamically register an MCP client, POST to the registration endpoint:

```typescript
const registrationBody = {
  redirect_uris: ["https://myapp.example/callback"],
  // Omit token_endpoint_auth_method for public clients
};

const resp = await fetch(
  "https://openseo.example.com/api/auth/oauth2/register",
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(registrationBody),
  }
);

const clientInfo = await resp.json(); 
// Returns client_id, and client_secret for confidential clients

```

### Initiating Authorization

Construct the authorization URL with the mandatory `mcp` scope:

```typescript
const authUrl = new URL("https://openseo.example.com/api/auth/oauth2/authorize");
authUrl.searchParams.set("response_type", "code");
authUrl.searchParams.set("client_id", clientInfo.client_id);
authUrl.searchParams.set("redirect_uri", "https://myapp.example/callback");
authUrl.searchParams.set("scope", "mcp read write");
authUrl.searchParams.set("state", "xyz"); // CSRF protection
// Redirect browser to authUrl

```

### Exchanging Codes for Tokens

Exchange the authorization code for access and refresh tokens:

```typescript
const tokenResp = await fetch(
  "https://openseo.example.com/api/auth/oauth2/token",
  {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code: authCode,
      redirect_uri: "https://myapp.example/callback",
      client_id: clientInfo.client_id,
      // client_secret: only for confidential clients
    }),
  }
);

const { access_token, refresh_token, expires_in } = await tokenResp.json();

```

### Refreshing Access Tokens

Use the refresh token to obtain new access tokens without user interaction:

```typescript
const refreshResp = await fetch(
  "https://openseo.example.com/api/auth/oauth2/token",
  {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "refresh_token",
      refresh_token: refreshToken,
      client_id: clientInfo.client_id,
      // client_secret: required for confidential clients
    }),
  }
);

const { access_token, expires_in } = await refreshResp.json();

```

## Summary

- **Endpoint Structure**: The provider exposes `/api/auth/oauth2/authorize`, `/token`, and `/register` through constants defined in [`src/server/mcp/oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts).
- **Registration Normalization**: The `normalizeClientRegistrationRequest` function in [`src/server/mcp/oauth-registration.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-registration.ts) distinguishes public clients (`none`) from confidential clients like Perplexity (`client_secret_post`).
- **Scope Enforcement**: The `tokenExchangeCallback` in [`src/server/mcp/oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts) throws `invalid_scope` errors for any request missing the mandatory `mcp` scope.
- **Token Lifetimes**: Access tokens expire in 24 hours, refresh tokens in 30 days, and client registrations persist for one year.
- **Context Persistence**: User identity and organization data are stored in KV via `createWorkersOAuthMcpProps` and restored during token exchanges.
- **Maintenance**: The `purgeExpiredData` method performs automated garbage collection of expired KV entries in batches of 200 keys.

## Frequently Asked Questions

### What is the difference between public and confidential MCP clients in OpenSEO?

**Public clients** omit the `token_endpoint_auth_method` in registration requests and are automatically assigned the `none` authentication method, making them suitable for browser-based applications. **Confidential clients**, such as those used by Perplexity, explicitly require secrets and receive the `client_secret_post` method to authenticate at the token endpoint, as determined by the normalization logic in [`src/server/mcp/oauth-registration.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-registration.ts).

### Why does every token request require the `mcp` scope?

The `tokenExchangeCallback` function in [`src/server/mcp/oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts) explicitly validates that `requestedScope.includes(MCP_SCOPE)` returns true before issuing tokens. This enforcement ensures that all access tokens issued through the OpenSEO provider maintain compatibility with the Managed Cloud Platform API requirements, preventing unauthorized scope elevation.

### How long do MCP access tokens and client registrations remain valid?

According to the provider configuration in [`src/server/mcp/oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts), **access tokens** expire after 24 hours, **refresh tokens** remain valid for 30 days, and **dynamic client registrations** persist for one year (365 days) before automatic expiration and garbage collection.

### Which package provides the underlying OAuth 2.0 implementation?

The OpenSEO application depends on **`@cloudflare/workers-oauth-provider`**, an open-source package that handles the core OAuth 2.0 logic. The `every-app/open-seo` repository adds a thin compatibility layer—including the `normalizeClientRegistrationRequest` shim and MCP-specific context handling—to adapt this package for Managed Cloud Platform integrations.