Skip to content

Add OAuth 2.1 + PKCE + DCR authorization to MCP endpoint#633

Open
Ortes wants to merge 3 commits intodocusealco:masterfrom
Fullphysio:oauth-mcp-pr
Open

Add OAuth 2.1 + PKCE + DCR authorization to MCP endpoint#633
Ortes wants to merge 3 commits intodocusealco:masterfrom
Fullphysio:oauth-mcp-pr

Conversation

@Ortes
Copy link
Copy Markdown

@Ortes Ortes commented Apr 20, 2026

Summary

Adds OAuth 2.1 authorization to the existing POST /mcp endpoint so MCP clients that only support OAuth (notably the Claude.ai web-UI custom-connector flow) can authenticate without a pre-shared bearer token. The existing McpToken bearer flow is preserved as a fallback for CLI/desktop clients.

Re: #630 — I understand the issue was closed as not planned; opening this PR as a concrete reference in case it's useful, and to give self-hosters who want the feature a clean branch to pull from.

Implementation

Doorkeeper 5.9 is mounted alongside the existing Devise stack, reusing the oauth_applications / oauth_access_grants / oauth_access_tokens tables that already ship in this repo (migrations 20240720063827_create_doorkeeper_tables.rb and 20260224120000_add_pkce_to_doorkeeper_access_grants.rb). No new migrations.

  • OAuth 2.1 + PKCE (S256 required) via Doorkeeper's force_pkce, 1-hour access tokens, hashed token secrets.
  • RFC 7591 Dynamic Client Registration at POST /register — custom controller (Doorkeeper doesn't ship DCR). Public clients only, client_secret_expires_at: 0, IP-based throttle (20/hour), redirect_uri restricted to https:// or loopback (127.0.0.1, localhost, ::1) per OAuth 2.1 §8.4.2.
  • RFC 8414 Authorization Server Metadata at /.well-known/oauth-authorization-server.
  • RFC 9728 Protected Resource Metadata at /.well-known/oauth-protected-resource, advertised via WWW-Authenticate: Bearer resource_metadata="…" on MCP 401 responses.
  • Root-path aliases (/authorize, /token, /register) for clients that strip paths during discovery.
  • McpController#current_user resolves a Doorkeeper access token with scope mcp first, then falls back to the existing McpToken bearer.
  • A weekly sweeper job (OauthApplicationSweeperJob) deletes DCR applications older than 90 days with no active tokens (externally scheduled).
  • Connected apps link added to the MCP settings page so users can revoke authorized applications.

Tests

Full RSpec coverage (16 examples, all green) under spec/requests/:

  • well_known_spec.rb — discovery endpoint shapes
  • mcp_oauth_spec.rb — valid / expired / revoked / wrong-scope / missing token; legacy McpToken fallback; WWW-Authenticate header on 401
  • oauth_register_spec.rb — DCR happy path, loopback redirect, non-loopback rejection, empty/malformed body, throttle
  • oauth_flow_spec.rb — end-to-end register → authorize (PKCE) → token → POST /mcp
  • spec/jobs/oauth_application_sweeper_job_spec.rb

A Dockerfile.test + docker-compose.test.yml are included so reviewers can run the suite without installing libvips/libpdfium/onnxruntime locally:

docker compose -f docker-compose.test.yml up --build --exit-code-from test

Compatibility notes

  • Existing McpToken bearer flow is unchanged — the OAuth resolution runs first and falls through on miss.
  • No new migrations (the Doorkeeper tables already exist in the repo).
  • New gem: doorkeeper ~> 5.9.

Test plan

  • docker compose -f docker-compose.test.yml up --build --exit-code-from test passes
  • Existing MCP bearer flow still works with a pre-existing McpToken
  • Register a client via POST /register, run the full auth-code + PKCE flow, call POST /mcp with the resulting access token
  • curl /.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource return valid JSON
  • POST /mcp without a token returns 401 with WWW-Authenticate: Bearer resource_metadata="…"

🤖 Generated with Claude Code

Ortes and others added 3 commits April 20, 2026 07:49
Adds Doorkeeper-backed OAuth 2.1 (PKCE, public clients, RFC 7591 DCR)
so Claude connectors can authorize against DocuSeal without a
pre-shared token. The existing McpToken bearer stays as a fallback.

- Mount Doorkeeper at /oauth/* plus root aliases (/authorize, /token,
  /register) for clients that strip paths
- Serve RFC 8414 + RFC 9728 discovery at /.well-known/oauth-*
- /register implements RFC 7591 DCR for public clients with an IP
  throttle; redirect_uri restricted to https + loopback
- McpController now resolves current_user from a Doorkeeper access
  token first, emits the RFC 9728 WWW-Authenticate header on 401
- Weekly sweeper for abandoned DCR applications (external cron)
- Link Connected apps from MCP settings

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cker

Existing Dockerfile builds the production image with
BUNDLE_WITHOUT=development:test, so it can't run RSpec. This adds a
test-only image that ships every native lib the app loads at boot
(libvips, libpdfium, onnxruntime, chromium for cuprite) plus the full
dev/test gem bundle.

Usage:
  docker compose -f docker-compose.test.yml up --build
  # or run a subset:
  SPEC_FILES="spec/requests/well_known_spec.rb" \
    docker compose -f docker-compose.test.yml up --build --exit-code-from test

The bundle gems live in a named volume so `docker compose up` re-runs
are fast after the first build. Postgres uses tmpfs — no state
persists between runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rottle test

Three issues surfaced running the suite in docker:

- hash_token_secrets stores the access token hashed; specs must use
  access_token.plaintext_token (not .token) when posing as a client
- Doorkeeper's Application model rejects non-HTTPS redirect_uri by
  default; add force_ssl_in_redirect_uri to allow loopback per OAuth 2.1
- test env uses :null_store, so Rails.cache.increment returned nil and
  the DCR throttle never fired — stub a real MemoryStore in that spec

Also slim Dockerfile.test: drop chromium + chromium-chromedriver
(unused by OAuth specs, added ~4min to the build). Add a comment
pointing at the apk line to re-enable them for system specs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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