Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/mcp/shared/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,10 @@ def validate_scope(self, requested_scope: str | None) -> list[str] | None:

def validate_redirect_uri(self, redirect_uri: AnyUrl | None) -> AnyUrl:
if redirect_uri is not None:
# Validate redirect_uri against client's registered redirect URIs
if self.redirect_uris is None or redirect_uri not in self.redirect_uris:
# Validate redirect_uri against client's registered redirect URIs.
# Pydantic URL equality is type-strict across AnyUrl subclasses, so
# compare canonical serialized values instead of object identity.
if self.redirect_uris is None or str(redirect_uri) not in {str(uri) for uri in self.redirect_uris}:
raise InvalidRedirectUriError(f"Redirect URI '{redirect_uri}' not registered for client")
return redirect_uri
elif self.redirect_uris is not None and len(self.redirect_uris) == 1:
Expand Down
31 changes: 29 additions & 2 deletions tests/shared/test_auth.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
"""Tests for OAuth 2.0 shared code."""

import pytest
from pydantic import ValidationError
from pydantic import AnyHttpUrl, AnyUrl, ValidationError

from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthMetadata
from mcp.shared.auth import (
InvalidRedirectUriError,
OAuthClientInformationFull,
OAuthClientMetadata,
OAuthMetadata,
)


def test_oauth():
Expand Down Expand Up @@ -138,3 +143,25 @@ def test_invalid_non_empty_url_still_rejected():
}
with pytest.raises(ValidationError):
OAuthClientMetadata.model_validate(data)


def test_redirect_uri_validation_accepts_equivalent_pydantic_url_types():
"""Pydantic URL subclasses with the same serialized URI should match."""
client_info = OAuthClientInformationFull(
client_id="client-1",
redirect_uris=[AnyHttpUrl("https://example.com/callback")],
)

redirect_uri = client_info.validate_redirect_uri(AnyUrl("https://example.com/callback"))

assert str(redirect_uri) == "https://example.com/callback"


def test_redirect_uri_validation_rejects_unregistered_equivalent_type():
client_info = OAuthClientInformationFull(
client_id="client-1",
redirect_uris=[AnyHttpUrl("https://example.com/callback")],
)

with pytest.raises(InvalidRedirectUriError):
client_info.validate_redirect_uri(AnyUrl("https://evil.example/callback"))
Loading