How OpenSEO's OAuth Provider Works for Hosted Authentication

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

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
// 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. 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.

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

// 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

The consent UI (src/routes/_authenticated.oauth-consent.tsx) displays the requested scopes and captures user approval:

// 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 }),
  });
};

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

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

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

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:

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

// 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 (lines 96‑100) validates the deployment environment:

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

// 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), 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 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.

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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →