Skip to content

feat(mcp): OAuth 2.1 (DCR + PKCE) for ChatGPT and claude.ai connectors on /api/mcp - #449

Merged
ralyodio merged 1 commit into
masterfrom
mcp-oauth
Nov 11, 2026
Merged

ralyodio merged 1 commit into
masterfrom
mcp-oauth

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Makes the CrawlProof MCP server listable in the Claude Connectors Directory and ChatGPT, which accept only no-auth or OAuth 2.1. Ported from ThreatCrush #450.

Connector URL: https://crawlproof.com/api/mcp (unchanged). Existing crp_ API keys keep working there; an OAuth access token works too.

What

  • /.well-known/oauth-protected-resource (+ /api/mcp suffix): resource = https://crawlproof.com/api/mcp exactly, authorization_servers[0] = https://crawlproof.com
  • /.well-known/oauth-authorization-server (+ suffix): registration_endpoint, S256 only, code, authorization_code + refresh_token, token_endpoint_auth_methods_supported: ["none"]
  • POST /api/oauth/register: RFC 7591 DCR, public clients only; https redirects (claude.ai, chatgpt.com, ...) and http loopback with any port (RFC 8252)
  • /oauth/authorize: consent page on the existing Supabase session (password, Google, CoinPay sign-in all bind by Supabase user id, never by CoinPay email). Signed out -> /login?redirect= round trip. Unknown client/redirect is shown, never followed. Names the client. Not frameable.
  • POST /api/oauth/authorize: the consent form target; same-origin only (Origin / Sec-Fetch-Site); 303 back with code, state, iss (RFC 9207)
  • POST /api/oauth/token: form-encoded (JSON too); single-use 5-min code bound to client + redirect + PKCE; rotating refresh; POST /api/oauth/revoke (RFC 7009)
  • crp_oat_ access tokens (1h) act as the approving user, resolved only by /api/mcp; refresh 90d; hashes only
  • /api/mcp: 401 WWW-Authenticate: Bearer resource_metadata="https://crawlproof.com/.well-known/oauth-protected-resource" pinned to the public origin; tools/list now carries resultType/ttlMs/cacheScope (Claude Code requires them)
  • proxy.ts: OAuth machine endpoints skip the crawl gateway and the Supabase session refresh
  • API tokens page: "Connected apps" card (list + disconnect)
  • Migration 20261111140000_mcp_oauth.sql: oauth_clients, oauth_codes, oauth_tokens, RLS on, service role only

Tests

tests/mcp-oauth.test.ts (19): discovery docs, redirect rules, DCR (public ok, confidential/bad redirect refused), consent (cross-site refused, signed-out -> login, unregistered redirect never followed, deny, missing PKCE), code -> token (form-encoded) -> refresh rotation -> replay refused -> grants list/disconnect, revoke, loopback any port, /api/mcp 401 + resource_metadata, OAuth token lists tools with cache fields, existing crp_ key still lists tools.
Local: bun run typecheck clean; full suite 3083 passed, 9 skipped.

🤖 Generated with Claude Code

… can sign in to /api/mcp

Ported from ThreatCrush (#450). Existing crp_ API keys keep working on
/api/mcp; an OAuth access token (crp_oat_, 1h, rotating 90-day refresh) is
accepted there too and nowhere else.

- /.well-known/oauth-protected-resource (+ /api/mcp suffix): resource is
  exactly https://crawlproof.com/api/mcp, AS = crawlproof.com
- /.well-known/oauth-authorization-server: DCR, S256, code, refresh, public
- POST /api/oauth/register (RFC 7591), /api/oauth/token (form or JSON),
  /api/oauth/revoke (RFC 7009), /api/oauth/grants (connected apps)
- /oauth/authorize consent page on the Supabase session; signed-out users go
  through /login and back; same-origin POST; iss in every redirect (RFC 9207)
- 401 resource_metadata pinned to the public origin; tools/list carries
  resultType/ttlMs/cacheScope
- OAuth endpoints skip the crawl gateway and session refresh in proxy.ts
- Connected apps card on the API tokens page
- migration 20261111140000_mcp_oauth.sql (hashes only, service role only)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

50 finding(s)

HIGH/CRITICAL: 2 | MEDIUM: 30 | LOW: 18

Severity Rule Location
HIGH tls-verification-disabled lib/onion.ts:50
HIGH secret-generic-credential lib/sp/platforms/facebook.ts:32
MEDIUM js-unescaped-html-sink app/(app)/dashboard/admin/email-broadcast/EmailBroadcastForm.tsx:125
MEDIUM js-unescaped-html-sink app/(app)/dashboard/projects/[id]/autoblog/articles/[articleId]/page.tsx:214
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:74
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:102
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:177
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:181
MEDIUM js-unescaped-html-sink app/c/[project]/[slug]/page.tsx:76
MEDIUM js-unescaped-html-sink app/c/[project]/page.tsx:58
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:228
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:285
MEDIUM js-unescaped-html-sink app/layout.tsx:139
MEDIUM js-open-redirect app/login/form.tsx:40
MEDIUM js-unescaped-html-sink app/r/[token]/page.tsx:164
MEDIUM js-open-redirect app/signup/form.tsx:65
MEDIUM js-unescaped-html-sink app/surveys/[slug]/invite/page.tsx:68
MEDIUM js-unescaped-html-sink components/json-ld.tsx:8
MEDIUM js-unescaped-html-sink components/pit-footer.tsx:17
MEDIUM js-unescaped-html-sink components/report/markdown-view.tsx:16
MEDIUM js-unescaped-html-sink lib/careers/page-templates.ts:198
MEDIUM js-dynamic-code-execution lib/crawl-limits.ts:67
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:41
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:324
MEDIUM redos-nested-quantifier lib/lx/articleGen.ts:100
MEDIUM redos-nested-quantifier lib/tracker/agent-gate.ts:61
MEDIUM sh-predictable-temp-path ops/selfhost/server/setup-supabase.sh:218
MEDIUM sql-template-interpolation scripts/detect-slot-themes.ts:31
MEDIUM sql-template-interpolation scripts/purge-constructed-keywords.ts:163
MEDIUM sql-template-interpolation scripts/purge-offniche-keywords.ts:124
MEDIUM js-dynamic-code-execution scripts/test-crawl-limits.mjs:14
MEDIUM js-dynamic-code-execution scripts/test-crawl-limits.mjs:24
LOW secret-generic-credential app/(marketing)/docs/autoblog-webhook/page.tsx:156
LOW secret-generic-credential lib/sealed/migrate.ts:38
LOW secret-generic-credential lib/sp/platforms/linkedin.ts:25
LOW secret-generic-api-key tests/ads-generator-agent.test.ts:160
LOW secret-generic-api-key tests/ads-generator-agent.test.ts:187
LOW js-dynamic-code-execution tests/careers-page-templates.test.ts:21
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:19
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:69
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:51
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:52
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:20
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:24
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:25
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:26
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:31
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:35
LOW secret-generic-credential tests/contract/posthog-integration.test.ts:13
LOW secret-generic-credential tests/lead-campaign.test.ts:16

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 00f1323 into master Nov 11, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant