Skip to content

MCP OAuth 2.1 ceremonial flow for claude.ai connector - #31

Merged
jpr5 merged 15 commits into
mainfrom
feat/mcp-oauth
Apr 20, 2026
Merged

MCP OAuth 2.1 ceremonial flow for claude.ai connector#31
jpr5 merged 15 commits into
mainfrom
feat/mcp-oauth

Conversation

@jpr5

@jpr5 jpr5 commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Implements full OAuth 2.1 ceremonial flow (RFC 6749 + 7591 + 7636 + 8414 + 9728) so claude.ai and other MCP clients can complete the authentication handshake against the Pathfinder MCP server.
  • Anonymous OAuth: all endpoints are RFC-compliant, but /authorize auto-approves and tokens carry sub: "anonymous". Downstream code must not treat sub as a user identifier.
  • Opportunistic bearer auth on /mcp — requests WITHOUT an Authorization header still succeed unchanged. Only invalid/expired tokens are rejected. Existing production clients are not affected.
  • Zero new runtime dependencies: HS256 JWT is hand-rolled with node:crypto in src/oauth/jwt.ts (~150 lines) and trivially swappable for jose later if asymmetric keys are ever needed.
  • Per-endpoint per-IP fixed-window rate limiting: /register 10/min, /authorize 30/min, /token 30/min — returns 429 + Retry-After on exceed.
  • PKCE S256 only (rejects plain), timing-safe verifier comparison, redirect_uri validated at both /authorize and /token, one-time-use codes with 10-minute TTL.

Required pre-merge step

Set MCP_JWT_SECRET on Railway copilotkit-docs service BEFORE merging. Generate with:

openssl rand -hex 32

Without this, production startup will fail with MCP_JWT_SECRET is required in production. In development the secret is auto-generated at startup (ephemeral — all tokens invalidated on restart).

New files

  • src/oauth/jwt.ts — HS256 sign/verify with base64url, clock-skew handling, typed error classes
  • src/oauth/store.ts — in-memory clientStore + codeStore singletons (lazy expiry at consume time)
  • src/oauth/secret.tsresolveJwtSecret({nodeEnv}) with dev/prod policy + cache
  • src/oauth/rate-limiter.tsOAuthRateLimiter class + three per-endpoint singletons
  • src/oauth/handlers.ts — metadata, register, authorize, token handlers + opportunistic bearer middleware

Modified files

  • src/server.ts — replaces the old stub OAuth endpoints with real handlers, mounts bearerMiddleware on /mcp POST/DELETE, adds express.urlencoded after express.json
  • src/config.ts — adds mcpJwtSecret: string to the Config interface, resolves via resolveJwtSecret({nodeEnv}) in parseConfig()
  • .env.example — documents MCP_JWT_SECRET
  • Existing test mocks updated with mcpJwtSecret to satisfy Config typing

Test plan

  • JWT sign/verify unit tests (12) — green
  • In-memory store unit tests (10) — green
  • Secret resolution unit tests (6) — green
  • Rate limiter unit tests (5) — green
  • Handler unit tests (30) — metadata, register, authorize, token, bearer middleware — green
  • End-to-end integration test (3) — full register→authorize→token→/mcp flow using a real Express server + built-in fetch; covers opportunistic-no-auth and garbage-token 401 paths
  • npm test — 2364 passing / 2368 total (4 pre-existing failures in analytics-server.test.ts — 2 unique tests × 2 because tsc emits tests into dist/; present on origin/main unchanged, unrelated to OAuth)
  • npm run build — clean, zero TypeScript errors
  • npx prettier --check "src/**/*.ts" — clean
  • No as any typecasting introduced
  • Post-deploy manual verification:
    • curl https://mcp.copilotkit.ai/.well-known/oauth-protected-resource returns authorization_servers
    • curl https://mcp.copilotkit.ai/.well-known/oauth-authorization-server returns full AS metadata
    • curl -X POST https://mcp.copilotkit.ai/register -H 'Content-Type: application/json' -d '{"redirect_uris":[]}' returns 201
    • In claude.ai, add the connector and click Authenticate — succeeds
    • Unauthenticated POST /mcp still works (no regression)

Observability

All OAuth events log with [oauth] prefix. INFO on successful register/authorize/token (client_id, IP). WARN on PKCE failures, unknown clients, expired codes, and rate limits. Tokens and code verifiers are never logged.

Security details

  • PKCE mandatory, S256 only
  • Timing-safe comparisons (crypto.timingSafeEqual) on PKCE verification and JWT signature
  • Redirect URI validated at both /authorize and /token
  • Codes are one-time use (deleted on consume attempt even if expired/mismatched)
  • HS256 secret rotation via env var + redeploy invalidates all outstanding tokens

@jpr5
jpr5 merged commit b767db0 into main Apr 20, 2026
5 checks passed
@jpr5
jpr5 deleted the feat/mcp-oauth branch April 20, 2026 17:20
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