[server][integrations] OAuth 2.1 for MCP + Cloudflare Worker proxy#238
Open
tswicegood wants to merge 17 commits intoNateBJones-Projects:mainfrom
Open
[server][integrations] OAuth 2.1 for MCP + Cloudflare Worker proxy#238tswicegood wants to merge 17 commits intoNateBJones-Projects:mainfrom
tswicegood wants to merge 17 commits intoNateBJones-Projects:mainfrom
Conversation
Adds OAuth 2.1 with PKCE, dynamic client registration (RFC 7591), and authorization-server metadata (RFC 8414) as an additive auth path on the core MCP server. Legacy x-brain-key header and ?key= query param keep working untouched — users can migrate clients one at a time. Motivation: the ?key= URL-param form leaks credentials into proxy logs, browser history, and Referer headers. OAuth replaces it with short-lived bearer JWTs signed by a server-side secret. Single-user design: one shared OAUTH_PASSWORD (falls back to MCP_ACCESS_KEY), JWTs signed with OAUTH_JWT_SECRET, stateless (no DB tables). Rotating OAUTH_JWT_SECRET invalidates every outstanding token as a panic button. Endpoints added: - GET /.well-known/oauth-authorization-server - POST /register (dynamic client registration) - GET /authorize (password prompt) - POST /authorize (verify password, issue code) - POST /token (authorization_code + refresh_token grants) docs/01-getting-started.md: new optional Step 7b with setup, redeploy, and panic-button rotation instructions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Supabase Edge Functions mount at /functions/v1/<name>/... so Hono
sees that full prefix on every request. The absolute routes
registered in registerOAuthRoutes() (/.well-known/..., /register,
/authorize, /token) never matched, and every OAuth request fell
through to the catch-all auth middleware and got a 401.
Refactor to a single app.use("*") middleware that dispatches by
path suffix and HTTP method. This works for any mount path without
configuration and keeps the module reusable across Edge Functions.
Body-parsing is gated on the path+method match so the MCP transport
still sees an intact request body on pass-through.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Supabase Edge Functions see internal URLs (http scheme, no /functions/v1/ prefix). The discovery doc was advertising those broken URLs to clients. Rebuild from SUPABASE_URL + function name (or OAUTH_ISSUER_URL override if set, or X-Forwarded-* headers as last fallback for self-hosters). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MCP clients (Claude Desktop, etc.) discover the OAuth authorization
server via the WWW-Authenticate challenge header returned on 401
from the MCP endpoint. The header points at a RFC 9728 protected-
resource metadata document, which lists the authorization_servers.
Adds:
- GET /.well-known/oauth-protected-resource (RFC 9728)
- WWW-Authenticate: Bearer realm="...", resource_metadata="..."
header on 401 responses from the catch-all auth middleware
- Access-Control-Expose-Headers: WWW-Authenticate in CORS so
browser-based MCP clients (claude.ai) can read it
Also refactors publicRoot() to take the request directly instead
of a path — the function-name segment is always the first path
segment regardless of which endpoint is being handled.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The MCP TypeScript SDK (Claude Desktop, mcp-remote) strips the URL path when composing OAuth discovery and endpoint URLs. Since Supabase Edge Functions mount under /functions/v1/<name>/, the SDK ends up probing https://<ref>.supabase.co/.well-known/... and /register at the bare origin, which Supabase's platform gateway 404s. OAuth never completes. This adds a tiny Cloudflare Worker that gives each MCP server a clean, path-less workers.dev origin. Every request proxies through to the Supabase function with /functions/v1/<name>/ prepended; responses pass through verbatim. The SDK has no path to strip and OAuth resolves. Structure matches the existing integrations/ pattern (slack-capture, discord-capture): README.md walkthrough, metadata.json, source in src/, plus a wrangler.toml.example users copy locally. No real credentials committed; users substitute their own SUPABASE_REF at setup time. Also adds a short tip callout at the end of Step 7 in docs/01-getting- started.md pointing users worried about ?key= URL-leak at this integration as the token-based alternative. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Claude Desktop and mcp-remote strip the URL path during OAuth discovery (MCP TypeScript SDK behavior) and can't reach OAuth endpoints hosted under /functions/v1/<name>/. Point users at the cloudflare-oauth-proxy integration as the fix, and reclassify the old 7b.4 panic button as 7b.5. Also adds a warning to 7b.3 that the "without ?key=" flow only works directly for path-aware clients; Claude Desktop users need the proxy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…roxy URLs Supabase secrets are project-wide (CLI and Dashboard both), so a single OAUTH_ISSUER_URL can't distinguish between multiple MCP servers behind different Cloudflare Worker proxies within the same project. publicRoot() now checks OAUTH_ISSUER_URL_<FUNCTION_NAME_UPPER> first, then falls back to the plain OAUTH_ISSUER_URL. The core server uses the plain name (default) and extensions/recipes set the suffixed override only when they need a different value — matches the broader OB1 pattern of "core is default, extensions are the exception." Function name is derived from the first path segment of the incoming request, same logic already used for the URL-construction fallback. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…SUER_URL Supabase secrets are project-wide in both the CLI and Dashboard, not per-function as the earlier draft assumed. Rewrites Step 3 to match: the core server uses the plain OAUTH_ISSUER_URL and additional MCP servers (extensions, recipes) set a suffixed override like OAUTH_ISSUER_URL_LIFE_CRM_MCP. This follows the broader OB1 pattern where the core is the default and extensions are the exception. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…orize Supabase Edge Functions inject these headers by default: content-type: text/plain (overrides Hono's text/html) content-security-policy: default-src 'none'; sandbox x-content-type-options: nosniff That causes browsers to display the OAuth login form as raw source text (plus garbled UTF-8 per Latin-1 default) and blocks form submission (sandbox without allow-forms). Explicitly set all three on the /authorize GET and password-retry responses. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…assword
Supabase Edge Functions enforce text/plain + sandbox CSP on HTML
responses at the platform level — can't be overridden from inside the
function. Direct /authorize HTML rendering is broken.
Move the login-form rendering to the Cloudflare Worker proxy (see
integrations/cloudflare-oauth-proxy). The Edge Function now:
- GET /authorize (if hit directly, bypassing the Worker): return 501
text/plain pointing to the integration README. Claude Desktop's
flow never hits this since the Worker intercepts GET /authorize.
- POST /authorize wrong password: 302 back to /authorize with
?error=invalid_password and all original params echoed, so the
Worker re-renders the form with the error. Relative redirect so
it resolves against the Worker's origin, not Supabase's.
Removes now-unused loginPage + escapeHtml helpers and the HTML-headers
constant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Supabase Edge Functions force text/plain + sandbox CSP on HTML responses at the platform level; the OAuth login form was unreadable and unsubmittable through Supabase. Move form rendering to the Worker itself — it sees the same GET query params (client_id, code_challenge, etc.) via browser redirect, renders a form POSTing back to /authorize on the Worker origin, and proxies the POST to Supabase for password verification. Param validation (required params, S256 challenge method) moves to the Worker too, since the Worker decides whether to render or error. Form error display: if the Edge Function's POST handler sees a wrong password, it 302s back to GET /authorize?error=invalid_password&... which the Worker renders with an error message. ~100 lines added. No new dependencies. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Chrome was blocking the form submit with "Sending form data to ... violates the following Content Security Policy directive: 'form-action 'self''" — even though the form's action was same-origin. The form-action directive is strict about redirect chains (applies to every hop after submission) and our OAuth success flow redirects cross-origin to the MCP client's callback URL. Scoping form-action tightly enough to permit both same-origin submit AND arbitrary redirect_uri targets proved fragile across browsers. Per CSP Level 3, form-action doesn't fall back to default-src, so omitting it leaves form submission unrestricted. The password is the real security control here — CSP was defense-in-depth at best. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two targeted fixes to the auth module:
1. Env vars (OAUTH_PASSWORD, OAUTH_JWT_SECRET) were read at module load
into const bindings. Supabase Edge Function instances stay warm for
many requests, so the "rotate OAUTH_JWT_SECRET to invalidate all
outstanding tokens" panic button didn't take effect until cold start.
Replace the consts with small getPassword() / getJwtSecret() helpers
that call Deno.env.get() on every invocation — rotation now lands on
the next request, regardless of warm-instance lifetime.
2. Wrong-password attempts at POST /authorize had no log trail. Added a
single console.warn("[oauth] /authorize wrong password", {client_id})
right before the 302 redirect, so probe attempts show up in Supabase
Edge Function logs. Password is never logged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ML rationale Two doc fixes that make this integration drop cleanly into an upstream repo that doesn't know about the life-crm recipe: 1. README no longer assumes life-crm. Primary setup path is for the core Open Brain MCP server only (single Worker, single env var). A new "Multiple MCP servers" section explains the override naming pattern generically for anyone who runs additional Edge Functions — with an abstract "my-extension" example rather than a specific one. 2. README now explains the second reason the Worker exists (beyond path-stripping): Supabase Edge Functions force text/plain + sandbox CSP on every response at the platform level, which breaks any HTML the function tries to return. The Worker serves GET /authorize itself so the login form actually renders. Also: - wrangler.toml.example keeps only [env.core] as live config, with a commented-out block showing how to add a second Worker. - package.json drops the life-crm-specific scripts; generic deploy/tail point at the core env. - src/index.ts comment uses "open-brain-mcp" as the sole example. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds OAuth 2.1 authorization (RFC 6749 + PKCE) to the core open-brain MCP endpoint, with RFC 9728 protected-resource metadata, per-function issuer URL overrides, and CSP/content-type handling for the /authorize form. Designed to work behind the Cloudflare Worker proxy in a separate branch — together they enable Claude Desktop to connect via OAuth instead of ?key= URLs.
Adds a tiny Cloudflare Worker that fronts Supabase-hosted MCP servers on a path-less origin (ob-<name>.<account>.workers.dev) so the MCP TypeScript SDK's OAuth discovery — which strips URL paths when composing metadata URLs — can resolve correctly. Worker also serves GET /authorize HTML directly to bypass Supabase's platform-level Content-Type and CSP overrides that would otherwise break the login form. Pairs with the core OAuth 2.1 work in the merged-in oauth-mcp-server branch; OAUTH_ISSUER_URL secret on the Edge Function points at the Worker URL.
|
Hey @tswicegood — welcome to Open Brain Source! 👋 Thanks for submitting your first PR. The automated review will run shortly and check things like metadata, folder structure, and README completeness. If anything needs fixing, the review comment will tell you exactly what. Once the automated checks pass, a human admin will review for quality and clarity. Expect a response within a few days. If you have questions, check out CONTRIBUTING.md or open an issue. |
…uth-cloudflare-worker # Conflicts: # docs/01-getting-started.md
16 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Contribution Type
This PR spans two categories — checking both:
/integrations) — new Cloudflare Worker/server)(Not a recipe, schema, dashboard, or skill.)
What does this do?
Adds OAuth 2.1 (RFC 6749 + PKCE, with RFC 9728 protected-resource metadata) to the core open-brain MCP endpoint, and adds a tiny Cloudflare Worker proxy that gives Supabase-hosted MCP servers a path-less origin so the MCP TypeScript SDK's OAuth discovery resolves correctly. Together, these let Claude Desktop (and other MCP TS SDK clients like
mcp-remote) connect via OAuth instead of?key=URLs — the access key in?key=ends up in proxy logs, browser history, andRefererheaders, so a token-based path is meaningfully safer.The two pieces are separable in design but need each other in practice:
/.well-known/oauth-authorization-server,/register,/authorize). Supabase Edge Functions are mounted under/functions/v1/<name>/, so those URLs hit the platform gateway and 404. The server work is also blocked by Supabase's platform-level forcedContent-Type: text/plainanddefault-src 'none'; sandboxCSP on every response — fine for JSON, fatal for the HTML login form.So they're presented as one PR with two parallel tracks merged at the top —
[server]commits (OAuth on core MCP) and[integrations]commits (Worker proxy). The merge commits document the split intentionally.The flow on a working install:
?key=continues to work unchanged — both paths stay supported per client.Requirements
server/oauth.ts): no new dependencies. New env vars (all optional unless you opt into OAuth):OAUTH_PASSWORD— shared secret users type into the login formOAUTH_JWT_SECRET— HS256 signing key for issued access tokensOAUTH_ISSUER_URL— public URL where the MCP server is reachable (set to the Worker URL)OAUTH_ISSUER_URL_<FUNCTION_NAME>— per-function override for projects running multiple MCPs in one Supabase projectintegrations/cloudflare-oauth-proxy/): a Cloudflare account (free tier works),wranglerCLI, Node.js 20+. No paid services.Documentation:
integrations/cloudflare-oauth-proxy/README.md— Worker setup, including a "Multiple MCP servers" section for projects running additional Edge Functions.docs/01-getting-started.md— added a new optional Step 7b for OAuth setup, plus a tip pointing to the Worker.Checklist
README.mdwith prerequisites, step-by-step instructions, and expected outcomemetadata.jsonhas all required fields (Cloudflare Worker integration)wrangler.tomlis gitignored; onlywrangler.toml.exampleships)Test plan
integrations/cloudflare-oauth-proxy/README.mdSteps 1–2OAUTH_PASSWORD,OAUTH_JWT_SECRET, andOAUTH_ISSUER_URLsecrets on the Edge Function (Step 3)curl -s "${WORKER_URL}/.well-known/oauth-authorization-server" | jq .issuer— should return the Worker URL, not the Supabase URLcurl -sI "${WORKER_URL}/" | grep -i 'www-authenticate'— RFC 9728 header should reference the Worker-scoped resource_metadata URLnpx mcp-remote "${WORKER_URL}" --debug— full OAuth flow end-to-end, login form renders as HTML, password submission completes, tools list returns?key=in the URL?key=URL still works against the Supabase function directly (backwards compat)🤖 Generated with Claude Code