Skip to content

Fix OAuth consent CSP so Claude Desktop can connect to the MCP server#62

Merged
JanSchm merged 1 commit into
mainfrom
fix/mcp-oauth-consent-csp
Jun 9, 2026
Merged

Fix OAuth consent CSP so Claude Desktop can connect to the MCP server#62
JanSchm merged 1 commit into
mainfrom
fix/mcp-oauth-consent-csp

Conversation

@JanSchm

@JanSchm JanSchm commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes the MCP OAuth consent screen so Claude Desktop / claude.ai can complete the OAuth handshake against https://studio.brightbean.xyz/api/v1/mcp. Two changes:

  1. Adds the Claude callback hosts (claude.ai, claude.com) to the form-action CSP directive — scoped to the /oauth/authorize/ view only via csp_update, not site-wide.
  2. Replaces django-oauth-toolkit's stock consent template with a self-contained, BrightBean-branded one that drops a dead external CDN.

Why?

Connecting Claude died on the consent screen with two CSP console errors:

  • form-action (the blocker): approving consent issues a 302 redirect to https://claude.ai/api/mcp/auth_callback. Chromium enforces form-action across the entire redirect chain of a form submission, and claude.ai wasn't in our allowlist — so the authorization code never reached Claude and the connection silently failed. (Chrome reports the violation against the form's own action URL for privacy, which is why the console error confusingly named studio.brightbean.xyz even though 'self' is allowed — the real blocked hop was the redirect out to claude.ai.)
  • style-src (cosmetic): DOT's stock base.html loads Bootstrap 2.3.2 from the long-dead netdna.bootstrapcdn.com, which our style-src blocks → unstyled page + a second console error.

Scoping the form-action relaxation to the authorize view (rather than broadening the global policy) keeps every other form locked down. The redirect target is still validated by DOT against the registered client's redirect_uri + ALLOWED_REDIRECT_URI_SCHEMES=["https"], so this does not introduce an open redirect. All DOT form mechanics (csrf token, hidden fields, name="allow") are preserved in the template override.

How to test

  1. Unit: pytest apps/oauth_server apps/mcp/tests/test_oauth_auth.py — OAuth flow, PKCE S256, DCR, WWW-Authenticate challenge.
  2. CSP header (dev CSP is report-only, so assert the header directly): as an authenticated user with a registered public client, GET /oauth/authorize/?response_type=code&…&code_challenge_method=S256&scope=mcp and confirm the response's form-action now contains https://claude.ai https://claude.com, while a non-OAuth page does not (confirms the relaxation is scoped).
  3. Branded page: load the authorize URL in Chrome with DevTools open — both the netdna style-src error and the form-action error are gone, and the page renders on-brand. Click Authorize → redirects cleanly to https://claude.ai/api/mcp/auth_callback?code=….
  4. Real client: add the MCP server in Claude Desktop against https://studio.brightbean.xyz/api/v1/mcp and confirm it connects and lists tools.

Checklist

  • Tests pass (pytest) — 712 passed
  • Lint passes (ruff check . and ruff format --check .) — both clean repo-wide
  • Documentation updated (if applicable) — N/A (no user-facing docs affected)

The MCP OAuth consent screen silently failed in Claude's browser:
approving it 302-redirects to https://claude.ai/api/mcp/auth_callback,
but Chromium enforces form-action across the redirect chain and
claude.ai wasn't allowlisted, so the authorization code never reached
Claude. A second (cosmetic) console error came from django-oauth-toolkit's
stock template loading Bootstrap from the dead netdna.bootstrapcdn.com,
which style-src blocks.

- Scope form-action to allow claude.ai/claude.com on the authorize view
  only (csp_update appends to the global policy), not site-wide.
- Override the oauth2_provider templates with a self-contained,
  BrightBean-branded consent page that drops the external CDN. All DOT
  form mechanics (csrf token, hidden fields, name="allow") are preserved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@JanSchm JanSchm merged commit 16a842c into main Jun 9, 2026
5 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