# How OpenSEO's OAuth Provider Works for Hosted Authentication

> Discover how OpenSEO's OAuth provider leverages Cloudflare Workers and BetterAuth for secure hosted authentication. Securely manage user sessions and access tokens.

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

---

**OpenSEO's OAuth provider implements the full OAuth 2.0 authorization code flow on Cloudflare Workers, tightly integrating with BetterAuth to resolve user sessions, enforce organization context, and issue access tokens with mandatory MCP scope.**

The open-source [every-app/open-seo](https://github.com/every-app/open-seo) repository provides a reference implementation of a hosted MCP (Model Context Protocol) service with built-in authentication. For developers building multi-tenant SaaS applications, understanding how OpenSEO handles identity management through OAuth is essential. This article examines the complete authentication flow—from initial provider creation through token issuance—based on the actual source code.

## Architecture Overview

OpenSEO runs on **Cloudflare Workers** and uses the **`@cloudflare/workers-oauth-provider`** library as its OAuth foundation. The hosted authentication mode couples this provider with **BetterAuth** to obtain verified user sessions and organization context.

Two files form the core of this system:

- [`src/server/mcp/oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts) — the main OAuth provider implementation (~100 lines)
- [`src/middleware/ensure-user/hosted.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensure-user/hosted.ts) — BetterAuth session resolution and organization handling

## Provider Initialization and Context Resolution

### Lazy Provider Creation

The `createOpenSeoOAuthProvider` function builds an `OAuthProvider` instance on first request. It configures:

- The **resource URL** (derived from `BETTER_AUTH_URL` environment variable)
- Callback handlers for token exchange and client registration
- Props factory for user context injection

```typescript
// src/server/mcp/oauth-provider.ts (lines 38-45)
const oauthProvider = new OAuthProvider({
  apiRoute: "/api/auth/oauth2/",
  apiHostname: getHostedBaseUrl(),
  // ... additional configuration
});

```

### Resolving the Hosted User Context

Every request flows through `resolveHostedContext` in [`src/middleware/ensure-user/hosted.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensure-user/hosted.ts). This middleware:

1. Calls `getAuth().api.getSession()` to fetch the BetterAuth session
2. Verifies or creates a default organization for the user
3. Returns `{ userId, userEmail, organizationId }` for the OAuth flow

If no session exists, the user is redirected to `/sign-in` before continuing.

```typescript
// Conceptual flow based on src/middleware/ensure-user/hosted.ts
async function resolveHostedContext(req: Request) {
  const session = await getAuth().api.getSession({ headers: req.headers });
  if (!session) return { redirect: "/sign-in" };
  
  const organization = await ensureOrganization(session.user.id);
  return { userId: session.user.id, userEmail: session.user.email, organizationId: organization.id };
}

```

## The Authorization Flow

### Step 1: Authorization Endpoint

Clients initiate OAuth by redirecting users to:

```

GET /api/auth/oauth2/authorize?response_type=code&client_id=...&redirect_uri=...&scope=mcp&state=...

```

The `handleOAuthAuthorizeRequest` function (lines 51‑70) processes this:

```typescript
// src/server/mcp/oauth-provider.ts — handleOAuthAuthorizeRequest
const result = await oauth.parseAuthRequest(url);
if (!userContext.userId) {
  // Redirect to sign-in, preserving original parameters
  return redirectToSignIn(url);
}
// Build consent URL with original parameters encoded
return redirect(buildConsentUrl(url));

```

**Key behaviors:**
- **Unauthenticated users** → redirected to `/sign-in`
- **Authenticated users** → redirected to `/oauth-consent`

### Step 2: Consent Page

The consent UI ([`src/routes/_authenticated.oauth-consent.tsx`](https://github.com/every-app/open-seo/blob/main/src/routes/_authenticated.oauth-consent.tsx)) displays the requested scopes and captures user approval:

```tsx
// React component posting decision to backend
const submit = async (accept: boolean) => {
  await fetch("/api/oauth/consent", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ accept, query: originalParams }),
  });
};

```

### Step 3: Consent Validation and Authorization Completion

The `handleOAuthConsentResponse` function (lines 72‑90) validates the submission:

```typescript
// src/server/mcp/oauth-provider.ts — handleOAuthConsentResponse
const validated = csrfProtected(consentResponseSchema, body);
if (!validated.accept) {
  return oauth.completeError(redirectUri, "access_denied", state);
}

// Rebuild and re-parse the original authorize request
const authRequest = buildAuthorizeRequestFromConsentQuery(validated.query);
const scopes = getGrantedMcpScopes(authRequest);

// Complete with user context props
return oauth.completeAuthorization({
  request: authRequest,
  props: createWorkersOAuthMcpProps(userContext, baseUrl),
});

```

This generates an **authorization code** and redirects to the client's `redirect_uri` with `?code=...&state=...`.

## Token Exchange and Access Token Issuance

### The Token Endpoint

Clients exchange codes at `POST /api/auth/oauth2/token`:

```typescript
// Example token exchange request
const response = await fetch("https://app.openseo.so/api/auth/oauth2/token", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    grant_type: "authorization_code",
    code: authorizationCode,
    redirect_uri: "https://myapp.example.com/callback",
    client_id: "my-client-id",
    client_secret: "my-client-secret",
  }),
});

```

### Scope Enforcement

The `tokenExchangeCallback` (lines 16‑22) enforces critical validation:

```typescript
tokenExchangeCallback: async (props, grantedScopes) => {
  // Mandatory: all tokens must include 'mcp' scope
  if (!grantedScopes.includes("mcp")) {
    throw new Error("mcp scope is required");
  }
  return {
    ...props,
    grantedScopes, // Propagated to token properties
  };
}

```

**Every access token issued by OpenSEO's OAuth provider must include the `mcp` scope.** This ensures MCP API compatibility.

## Client Registration and API Key Short-circuit

### Dynamic Client Registration

OpenSEO supports **OAuth 2.0 Dynamic Client Registration** at `/api/auth/oauth2/register`:

```typescript
// src/server/mcp/oauth-provider.ts (lines 53-57)
async handleClientRegistrationRequest(request: Request) {
  const normalized = normalizeClientRegistrationRequest(await request.json());
  return oauth.handleClientRegistrationRequest(request, normalized);
}

```

Registered clients expire after **one year** (`MCP_CLIENT_REGISTRATION_TTL_SECONDS`).

### API Key Authentication (Pre-OAuth Bypass)

Before initiating the full OAuth flow, `handleMcpApiKeyRequest` (lines 50‑52) checks for API key headers:

```typescript
// Short-circuit for service-to-service authentication
const apiKeyResult = await handleMcpApiKeyRequest(request);
if (apiKeyResult) return apiKeyResult; // Skip OAuth entirely

```

This enables automated clients to bypass user consent flows.

## Infrastructure: Base URLs and Cleanup

### Hosted Base URL Configuration

The `getHostedBaseUrl()` function in [`src/lib/auth.ts`](https://github.com/every-app/open-seo/blob/main/src/lib/auth.ts) (lines 96‑100) validates the deployment environment:

```typescript
export function getHostedBaseUrl(): string {
  const url = env.BETTER_AUTH_URL;
  if (!url) throw new Error("BETTER_AUTH_URL is required");
  if (!url.startsWith("https://") && !url.includes("localhost")) {
    throw new Error("BETTER_AUTH_URL must use HTTPS");
  }
  return url;
}

```

### Garbage Collection

A periodic cron task `purgeExpiredData` (lines 64‑71) removes stale KV entries for:

- Expired authorization grants
- Orphaned access tokens
- Abandoned client registrations

## Complete Flow Example

```typescript
// 1. Client redirects user to OpenSEO
const authUrl = new URL("https://app.openseo.so/api/auth/oauth2/authorize");
authUrl.searchParams.set("response_type", "code");
authUrl.searchParams.set("client_id", "myapp");
authUrl.searchParams.set("redirect_uri", "https://myapp.com/callback");
authUrl.searchParams.set("scope", "mcp read");
authUrl.searchParams.set("state", crypto.randomUUID());
window.location.href = authUrl.toString();

// 2. After user consent, exchange code for token
const tokenRes = await fetch("https://app.openseo.so/api/auth/oauth2/token", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    grant_type: "authorization_code",
    code: receivedCode,
    redirect_uri: "https://myapp.com/callback",
    client_id: "myapp",
    client_secret: "secret",
  }),
});
const { access_token } = await tokenRes.json();

// 3. Use token for MCP API calls
const mcpData = await fetch("https://app.openseo.so/api/mcp/query", {
  headers: { Authorization: `Bearer ${access_token}` },
});

```

## Summary

- **OpenSEO's OAuth provider** is built on `@cloudflare/workers-oauth-provider` with BetterAuth integration for hosted authentication.

- **User context resolution** happens in `resolveHostedContext` ([`src/middleware/ensure-user/hosted.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensure-user/hosted.ts)), which fetches sessions and ensures organization membership.

- **Mandatory `mcp` scope** is enforced at token exchange—no exceptions.

- **Consent flow** uses CSRF-protected forms posting to `/api/oauth/consent`, with denial returning standard `access_denied` errors.

- **API key short-circuit** allows service accounts to bypass OAuth entirely via `handleMcpApiKeyRequest`.

- **KV garbage collection** runs periodically via `purgeExpiredData` to clean stale grants and tokens.

## Frequently Asked Questions

### What OAuth 2.0 grants does OpenSEO support?

OpenSEO implements the **authorization code grant** for browser-based flows. The `tokenExchangeCallback` in [`src/server/mcp/oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts) handles code-to-token exchange. Implicit and client credentials grants are not supported in the current implementation.

### How does OpenSEO handle users without organizations?

The `resolveHostedContext` middleware automatically creates a **default hosted organization** if the authenticated user has none. This guarantees that every OAuth context includes an `organizationId`, which is required for MCP resource isolation.

### Can I use OpenSEO's OAuth provider without BetterAuth?

The hosted implementation requires BetterAuth for session resolution. However, the core `OAuthProvider` from `@cloudflare/workers-oauth-provider` is generic—you could replace `resolveHostedContext` with custom session logic for alternative identity providers.

### What happens if a user denies consent?

The `handleOAuthConsentResponse` function validates the denial and calls `oauth.completeError` with the `access_denied` error code. The user is redirected back to the client's `redirect_uri` with `?error=access_denied&state=...` per OAuth 2.0 specification.