-
Notifications
You must be signed in to change notification settings - Fork 3k
Fix RFC 8252 Section 7.3 compliance for loopback redirect URIs #1934
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Fix RFC 8252 Section 7.3 compliance for loopback redirect URIs #1934
Conversation
The MCP SDK's OAuthClientMetadata.validate_redirect_uri() was performing exact string matching, which violates RFC 8252 Section 7.3 for loopback addresses. RFC 8252 Section 7.3 states: 'The authorization server MUST allow any port to be specified at the time of the request for loopback IP redirect URIs, to accommodate clients that obtain an available ephemeral port from the operating system at the time of the request.' Changes: - Added RFC 8252-compliant validation for loopback addresses - For localhost/127.0.0.1/::1: port is ignored, scheme/hostname/path must match - For non-loopback URIs: exact matching as before (no behavior change) - Added comprehensive test coverage (13 tests) This fix enables native OAuth clients (like GitHub Copilot CLI) to work properly with MCP servers that use ephemeral loopback redirect ports.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request implements RFC 8252 Section 7.3 compliance for OAuth redirect URI validation in the MCP SDK. The change allows native OAuth clients to use ephemeral ports for loopback redirect URIs, which is required by the RFC to accommodate clients that dynamically obtain available ports at runtime.
Changes:
- Enhanced
OAuthClientMetadata.validate_redirect_uri()to ignore port numbers when validating loopback addresses (localhost, 127.0.0.1, ::1) - Added comprehensive test suite with 13 tests covering loopback and non-loopback scenarios
- Maintained backward compatibility: non-loopback URIs continue using exact matching
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/mcp/shared/auth.py | Added RFC 8252-compliant loopback redirect URI validation with port-flexible matching for localhost/127.0.0.1/::1 |
| tests/shared/test_rfc8252_redirect_uri.py | Added comprehensive test suite covering loopback port flexibility, exact non-loopback matching, and edge cases |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| is_loopback = parsed_requested.hostname in ("localhost", "127.0.0.1", "::1", "[::1]") | ||
|
|
||
| if is_loopback: | ||
| for registered in self.redirect_uris: | ||
| parsed_registered = urlparse(str(registered)) | ||
| if parsed_registered.hostname not in ("localhost", "127.0.0.1", "::1", "[::1]"): |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The IPv6 loopback check includes "[::1]" with brackets in the hostname check, but Python's urlparse().hostname returns IPv6 addresses without brackets (e.g., "::1" not "[::1]"). This means the bracketed form will never match and IPv6 loopback URIs won't work correctly.
The is_loopback check on line 114 includes "[::1]" which will never be returned by parsed_requested.hostname. Similarly, line 119 checks for "[::1]" which will never match parsed_registered.hostname. Remove "[::1]" from both tuples and keep only "::1".
| is_loopback = parsed_requested.hostname in ("localhost", "127.0.0.1", "::1", "[::1]") | |
| if is_loopback: | |
| for registered in self.redirect_uris: | |
| parsed_registered = urlparse(str(registered)) | |
| if parsed_registered.hostname not in ("localhost", "127.0.0.1", "::1", "[::1]"): | |
| is_loopback = parsed_requested.hostname in ("localhost", "127.0.0.1", "::1") | |
| if is_loopback: | |
| for registered in self.redirect_uris: | |
| parsed_registered = urlparse(str(registered)) | |
| if parsed_registered.hostname not in ("localhost", "127.0.0.1", "::1"): |
| # Without trailing slash - Pydantic normalizes to include slash | ||
| result = client.validate_redirect_uri(AnyUrl("http://localhost:9999")) | ||
| # AnyUrl normalizes URLs, so both forms match | ||
| assert "localhost:9999" in str(result) |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider adding test coverage for case-insensitive hostname matching (e.g., "LOCALHOST", "LocalHost") to ensure the implementation correctly handles RFC 3986 case-insensitive hostname semantics. Python's urlparse().hostname normalizes to lowercase, so this should work, but explicit testing would confirm this behavior.
Problem
The MCP SDK's
OAuthClientMetadata.validate_redirect_uri()method performs exact string matching on redirect URIs, which violates RFC 8252 Section 7.3 for loopback addresses.RFC 8252 Section 7.3 states:
Impact
This prevents native OAuth clients (like GitHub Copilot CLI) from successfully completing OAuth flows with MCP servers, as these clients typically use ephemeral ports for their loopback redirect listeners.
Solution
This PR updates the validation logic to be RFC 8252-compliant:
Changes
Updated
src/mcp/shared/auth.py:urllib.parseimport for URL parsingvalidate_redirect_uri()with RFC 8252 logicAdded
tests/shared/test_rfc8252_redirect_uri.py:Testing
All 13 new tests pass:
Backward Compatibility
This change is fully backward compatible:
References
http://127.0.0.1:60847/varies per session)