Skip to content

feat(mcp): OAuth 2.1 (DCR + PKCE) so ChatGPT and claude.ai connectors can sign in to /api/v1/mcp - #189

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 PairUX MCP server at https://pairux.com/api/v1/mcp listable in the Claude Connectors Directory and as a ChatGPT app, which only accept no-auth or OAuth 2.1. Ported from ThreatCrush #450 via CrawlProof #449 (Supabase cookie-session variant).

Existing credentials keep working: pairux login CLI tokens (pux_at_) and PairUX sessions are accepted on /api/v1/mcp exactly as before. OAuth access tokens (pux_oat_) are accepted there and nowhere else.

What

  • Discovery: /.well-known/oauth-protected-resource (resource = exactly https://pairux.com/api/v1/mcp, AS = https://pairux.com) and /.well-known/oauth-authorization-server (DCR, S256 only, code, authorization_code + refresh_token, none); path-suffixed forms via rewrites()
  • POST /api/oauth/register: RFC 7591, public clients only; https redirects, http loopback any port (RFC 8252)
  • /oauth/authorize: consent page naming the client, on the Supabase session; signed-out -> /login?redirect= and back; posts to /api/oauth/authorize (same-origin check), 303 back with code, state, iss (RFC 9207). Never framed. CSP form-action widened on that one page so Chrome does not block the 303 to claude.ai / chatgpt.com / loopback.
  • POST /api/oauth/token: form-encoded (or JSON); single-use 5-min code bound to client, redirect_uri, PKCE S256; 1h access, 90-day rotating refresh. POST /api/oauth/revoke (RFC 7009). /api/oauth/grants + a Connected apps card in Settings.
  • /api/v1/mcp: 401 + WWW-Authenticate: Bearer realm="pairux", resource_metadata="https://pairux.com/.well-known/oauth-protected-resource" for POST and GET without a valid credential; tools/list carries resultType/ttlMs/cacheScope; every tool has title + readOnly/destructive/idempotent/openWorld hints; serverInfo title.
  • Middleware: OAuth machine endpoints skip the crawl gateway and session refresh.
  • Migration 20261111150000_mcp_oauth.sql: oauth_clients, oauth_codes, oauth_tokens (hashes only, RLS on, service role only, FKs to auth.users).

Tests

  • apps/web/src/app/api/oauth/__tests__/mcp-oauth.test.ts (23): discovery, redirect rules, DCR refusals, consent cross-site/signed-out/deny/no-PKCE, full code -> token (form-encoded) -> refresh rotation -> replay -> grants -> disconnect, revoke, loopback port, MCP 401 + resource_metadata (POST and GET), OAuth tools/list with cache fields and annotations, old CLI token still works, session still works, OAuth token not accepted by requestUser.
  • Full web suite: 120 files, 1226 passed; typecheck + eslint clean; pre-commit turbo build passed.

🤖 Generated with Claude Code

… can sign in to /api/v1/mcp

Ported from ThreatCrush (#450) via CrawlProof (#449). `pairux login` CLI
tokens (pux_at_) and PairUX sessions keep working on /api/v1/mcp; an OAuth
access token (pux_oat_, 1h, rotating 90-day refresh) is accepted there too
and nowhere else.

- /.well-known/oauth-protected-resource (+ path-suffixed form via rewrite):
  resource is exactly https://pairux.com/api/v1/mcp, AS = pairux.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, naming the client;
  signed-out users go through /login and back; same-origin POST; iss in every
  redirect (RFC 9207); never framed; form-action widened on that page only so
  the 303 to claude.ai/chatgpt.com/loopback is not blocked by CSP
- /api/v1/mcp: 401 + WWW-Authenticate resource_metadata for POST and GET
  without a valid credential; tools/list carries resultType/ttlMs/cacheScope;
  every tool has a title and readOnly/destructive/idempotent/openWorld hints
- OAuth machine endpoints skip the crawl gateway and session refresh
- Connected apps card in Settings
- migration 20261111150000_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: 4 | MEDIUM: 24 | LOW: 22

Severity Rule Location
HIGH sh-eval-expansion .githooks/pre-commit:33
HIGH js-electron-node-integration apps/desktop/src/main/window.ts:49
HIGH sh-unquoted-expansion-destructive apps/installer/scripts/install.sh:717
HIGH sh-unquoted-expansion-destructive apps/installer/scripts/install.sh:948
MEDIUM sh-remote-script-execution apps/installer/scripts/install.sh:693
MEDIUM sh-remote-script-execution apps/installer/scripts/install.sh:840
MEDIUM sh-remote-script-execution apps/installer/scripts/install.sh:842
MEDIUM sh-remote-script-execution apps/installer/scripts/install.sh:1138
MEDIUM sh-remote-script-execution apps/installer/scripts/install.sh:1140
MEDIUM sh-remote-script-execution apps/livekit/setup-livekit-server.sh:93
MEDIUM sh-remote-script-execution apps/turn/deploy-droplet.sh:62
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:52
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:79
MEDIUM js-unescaped-html-sink apps/web/src/app/c/[handle]/page.tsx:199
MEDIUM js-open-redirect apps/web/src/app/cli/authorize/consent.tsx:68
MEDIUM js-unescaped-html-sink apps/web/src/app/l/[joinCode]/page.tsx:146
MEDIUM js-unescaped-html-sink apps/web/src/app/l/[joinCode]/page.tsx:230
MEDIUM js-unescaped-html-sink apps/web/src/app/layout.tsx:163
MEDIUM js-unescaped-html-sink apps/web/src/app/live/page.tsx:154
MEDIUM js-unescaped-html-sink apps/web/src/app/page.tsx:98
MEDIUM js-unescaped-html-sink apps/web/src/app/pricing/page.tsx:263
MEDIUM js-open-redirect apps/web/src/app/settings/copilot/copilot-settings.tsx:144
MEDIUM js-unescaped-html-sink apps/web/src/app/u/[username]/page.tsx:285
MEDIUM js-open-redirect apps/web/src/components/billing/useCheckout.ts:81
MEDIUM js-unescaped-html-sink apps/web/src/components/universal-footer.tsx:29
MEDIUM js-open-redirect apps/web/src/hooks/useDesktopHandoff.ts:24
MEDIUM redos-nested-quantifier apps/web/src/lib/deliverable.ts:11
MEDIUM sql-template-interpolation packages/ai-core/src/prompts.ts:36
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:138
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:139
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:340
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:341
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:371
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:372
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:383
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:397
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:398
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:405
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:415
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:416
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:453
LOW secret-generic-credential apps/desktop/src/main/agent-cli/commands.test.ts:454
LOW secret-generic-credential apps/desktop/src/main/agent-cli/copilot.test.ts:92
LOW secret-generic-credential apps/livekit/fly.toml:12
LOW secret-generic-credential apps/turn/fly.toml:11
LOW secret-openai-key apps/web/src/lib/copilot/keys.test.ts:62
LOW secret-generic-credential apps/web/src/lib/youtubeAutoLive.test.ts:90
LOW sh-predictable-temp-path appstores/firetv/emulator-test.sh:23
LOW secret-generic-credential docs/API.md:747
LOW secret-generic-credential docs/API.md:753

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 3f8b35a into master Nov 11, 2026
14 checks passed
@ralyodio
ralyodio deleted the mcp-oauth branch November 11, 2026 14:02
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