Skip to content

Add OAuth support for claude.ai custom connectors - #3

Open
ryudoawaru wants to merge 1 commit into
mainfrom
oauth-support
Open

Add OAuth support for claude.ai custom connectors#3
ryudoawaru wants to merge 1 commit into
mainfrom
oauth-support

Conversation

@ryudoawaru

Copy link
Copy Markdown
Member

Why

claude.ai custom connectors only accept public HTTPS + OAuth — they can't send a static Authorization header. This plugin authenticated with a fixed Redmine API key, so claude.ai could never connect.

Redmine / RedMica 6.1+ ships doorkeeper 5.8 with /oauth/authorize + /oauth/token and an admin UI for OAuth applications, so no authorization server code is needed here — only accepting its tokens and making it discoverable.

What changed

  • authenticate_mcp_user! tries Doorkeeper.authenticate(request) first (sets oauth_scope the way Redmine core does), then falls back to the existing static API key path. Claude Code / Claude Desktop keep working unchanged.
  • 401 responses now carry WWW-Authenticate: Bearer resource_metadata="…" so clients can discover OAuth.
  • New unauthenticated discovery endpoints:
    • GET /.well-known/oauth-protected-resource (+ /mcp variant) — RFC 9728
    • GET /.well-known/oauth-authorization-server — RFC 8414
  • Removed POST /mcp/token — exchanging username+password for an API key is obsolete with OAuth, and it was an unthrottled password brute-force surface.
  • README (中/EN) + CLAUDE.md updated with the claude.ai setup steps.

No RFC 7591 dynamic client registration: claude.ai's Advanced settings accepts a manually pasted Client ID / Secret.

Two fixes needed to actually connect

  1. scopes_supported is advertised. Doorkeeper's default_scopes in Redmine is public permissions only, so a client that requests no scope gets denied by every tool.
  2. dns_rebinding_protection: false on the transport. The mcp gem's Host allowlist is loopback-only and returns 403 Invalid Host header for any real domain — this was hit in testing. Rails' own config.hosts already performs host authorization.

Tests

New test/integration/oauth_discovery_test.rb — 5 runs, 15 assertions, all passing. Covers both metadata documents, the 401 WWW-Authenticate header, and end-to-end tools/list with both a doorkeeper access token and a static API key.

$ bin/rails test plugins/redmine_mcp/test/integration/oauth_discovery_test.rb
5 runs, 15 assertions, 0 failures, 0 errors, 0 skips

Setup for reviewers

Redmine admin → Applications → New: redirect URI https://claude.ai/api/mcp/auth_callback, Confidential checked, Scopes empty. Paste the resulting Client ID / Secret into claude.ai → Connectors → Add custom connector → Advanced settings.

🤖 Generated with Claude Code

claude.ai only accepts public HTTPS + OAuth for custom connectors; it
cannot send a static Authorization header. Accept Redmine's built-in
doorkeeper tokens (Redmine/RedMica 6.1+) and advertise the OAuth server
so clients can discover it.

- McpController#authenticate_mcp_user! tries Doorkeeper.authenticate
  first, setting oauth_scope like Redmine core does, then falls back to
  the static API key path used by Claude Code / Claude Desktop.
- 401 responses carry WWW-Authenticate with the resource metadata URL.
- New unauthenticated discovery endpoints: RFC 9728 protected resource
  metadata and RFC 8414 authorization server metadata, pointing at
  doorkeeper's /oauth/authorize and /oauth/token.
- Advertise scopes_supported: doorkeeper's default_scopes only covers
  public permissions, so a client that requests no scope gets denied by
  every tool.
- Disable the transport's DNS rebinding protection: its Host allowlist
  is loopback-only and 403s any real deployment. Rails' config.hosts
  already does host authorization.
- Drop POST /mcp/token — obsolete with OAuth, and it was an unthrottled
  password brute-force surface.

Co-Authored-By: Claude Opus 5 (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.

2 participants