Skip to content

fix(auth): validate redirect_uri schemes at DCR registration (RFC 9700 §4.1.1, RFC 7591 §2)#2630

Open
CrypticCortex wants to merge 3 commits into
modelcontextprotocol:mainfrom
CrypticCortex:dcr-redirect-uri-scheme-validation
Open

fix(auth): validate redirect_uri schemes at DCR registration (RFC 9700 §4.1.1, RFC 7591 §2)#2630
CrypticCortex wants to merge 3 commits into
modelcontextprotocol:mainfrom
CrypticCortex:dcr-redirect-uri-scheme-validation

Conversation

@CrypticCortex
Copy link
Copy Markdown

Closes #2629.

Summary

Adds validate_registered_redirect_uri(url: AnyUrl) and wires it into RegistrationHandler.handle so each submitted redirect_uri is validated immediately after model_validate_json succeeds. On failure, returns 400 invalid_redirect_uri per RFC 7591 §3.2.2.

The helper mirrors the existing validate_issuer_url policy:

  • https is allowed.
  • http is allowed only for loopback hosts (localhost, 127.0.0.1, [::1]) per RFC 8252 §7.3.
  • Fragments are rejected per RFC 7591 §2 — including empty trailing-# fragments.
  • Query strings are permitted (explicitly allowed by RFC 7591 §2).

The helper lives in a new mcp.server.auth._redirect_uri module to avoid a circular import with handlers.register (the existing mcp.server.auth.routes re-exports it so the public import path stays stable).

Why now

OAuthClientMetadata.redirect_uris is typed list[AnyUrl], which Pydantic accepts for any well-formed URL — including javascript:, data:, file:, vbscript:, ftp:, cleartext http:// to non-loopback hosts, and https://...#. The handler stores those values verbatim and authorize-time validation accepts them by exact match. Issue #<ISSUE_NUM> has the full reproduction.

Tests

tests/server/auth/test_routes.py: 13 unit tests covering each accept/reject path of the new helper, mirroring the existing test_validate_issuer_url_* cases (includes an explicit empty-fragment test).

tests/server/auth/test_error_handling.py: 5 end-to-end tests posting against /register — three rejection cases (javascript:, cleartext-http://, fragment) and two acceptance cases (HTTPS, loopback HTTP).

Local: ./scripts/test green; 1190 passed, 100.00% coverage, strict-no-cover clean.

Compatibility

No public-API removal. The change tightens what /register accepts; clients submitting non-spec-compliant URIs will now get 400 invalid_redirect_uri where they previously got 201. Existing stored client records are not re-validated.

Related

Checks

  • uv run --frozen ruff format .
  • uv run --frozen ruff check .
  • uv run --frozen pyright
  • ./scripts/test — green, 100% coverage, strict-no-cover clean

Extracts validate_registered_redirect_uri to mcp.server.auth._redirect_uri
to avoid a circular import with handlers.register; re-exports from routes
to preserve the public test import path.
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.

DCR registration accepts redirect_uris with non-HTTPS / non-loopback / fragmented schemes

1 participant