@@ -642,6 +642,61 @@ async def test_resource_param_included_with_protected_resource_metadata(self, oa
642642 content = request .content .decode ()
643643 assert "resource=" in content
644644
645+ @pytest .mark .anyio
646+ async def test_reject_metadata_with_mismatched_origin (self , oauth_provider : OAuthClientProvider ):
647+ """Test RFC 9728 Section 3.3: reject metadata with different scheme, host, or port."""
648+ # Test different scheme
649+ response_wrong_scheme = httpx .Response (
650+ 200 ,
651+ content = b'{"resource": "http://api.example.com/v1/mcp", "authorization_servers": ["https://auth.example.com"]}' ,
652+ )
653+ result = await oauth_provider ._handle_protected_resource_response (response_wrong_scheme )
654+ assert result is False
655+ assert oauth_provider .context .protected_resource_metadata is None
656+
657+ # Test different host
658+ response_wrong_host = httpx .Response (
659+ 200 ,
660+ content = b'{"resource": "https://evil.example.com/v1/mcp", "authorization_servers": ["https://auth.example.com"]}' ,
661+ )
662+ result = await oauth_provider ._handle_protected_resource_response (response_wrong_host )
663+ assert result is False
664+ assert oauth_provider .context .protected_resource_metadata is None
665+
666+ # Test different port
667+ response_wrong_port = httpx .Response (
668+ 200 ,
669+ content = b'{"resource": "https://api.example.com:8080/v1/mcp", "authorization_servers": ["https://auth.example.com"]}' ,
670+ )
671+ result = await oauth_provider ._handle_protected_resource_response (response_wrong_port )
672+ assert result is False
673+
674+ # Ensure no metadata was set
675+ assert oauth_provider .context .protected_resource_metadata is None
676+
677+ @pytest .mark .anyio
678+ async def test_reject_metadata_with_invalid_path_hierarchy (self , oauth_provider : OAuthClientProvider ):
679+ """Test RFC 9728 Section 3.3: reject metadata where resource is child of server URL."""
680+
681+ # Invalid: resource is child path
682+ response_child_path = httpx .Response (
683+ 200 ,
684+ content = b'{"resource": "https://api.example.com/v1/mcp/subpath", "authorization_servers": ["https://auth.example.com"]}' ,
685+ )
686+ result = await oauth_provider ._handle_protected_resource_response (response_child_path )
687+ assert result is False
688+ assert oauth_provider .context .protected_resource_metadata is None
689+
690+ # Valid: resource is parent path
691+ response_parent_path = httpx .Response (
692+ 200 ,
693+ content = b'{"resource": "https://api.example.com/v1", "authorization_servers": ["https://auth.example.com"]}' ,
694+ )
695+ result = await oauth_provider ._handle_protected_resource_response (response_parent_path )
696+ assert result is True
697+ assert oauth_provider .context .protected_resource_metadata is not None
698+ assert str (oauth_provider .context .protected_resource_metadata .resource ) == "https://api.example.com/v1"
699+
645700
646701class TestRegistrationResponse :
647702 """Test client registration response handling."""
@@ -745,7 +800,7 @@ async def test_auth_flow_with_no_tokens(self, oauth_provider: OAuthClientProvide
745800 # Send a successful discovery response with minimal protected resource metadata
746801 discovery_response = httpx .Response (
747802 200 ,
748- content = b'{"resource": "https://api.example.com/mcp", "authorization_servers": ["https://auth.example.com"]}' ,
803+ content = b'{"resource": "https://api.example.com/v1/ mcp", "authorization_servers": ["https://auth.example.com"]}' ,
749804 request = discovery_request ,
750805 )
751806
0 commit comments