@@ -196,13 +196,47 @@ class TestOAuthFlow:
196196 """Test OAuth flow methods."""
197197
198198 @pytest .mark .anyio
199- async def test_discover_protected_resource_request (self , oauth_provider ):
200- """Test protected resource discovery request building."""
201- request = await oauth_provider ._discover_protected_resource ()
199+ async def test_discover_protected_resource_request (self , client_metadata , mock_storage ):
200+ """Test protected resource discovery request building maintains backward compatibility."""
201+ async def redirect_handler (url : str ) -> None :
202+ pass
203+
204+ async def callback_handler () -> tuple [str , str | None ]:
205+ return "test_auth_code" , "test_state"
202206
207+ provider = OAuthClientProvider (
208+ server_url = "https://api.example.com" ,
209+ client_metadata = client_metadata ,
210+ storage = mock_storage ,
211+ redirect_handler = redirect_handler ,
212+ callback_handler = callback_handler ,
213+ )
214+
215+ # Test without response (backward compatibility)
216+ request = await provider ._discover_protected_resource ()
203217 assert request .method == "GET"
204218 assert str (request .url ) == "https://api.example.com/.well-known/oauth-protected-resource"
205219 assert "mcp-protocol-version" in request .headers
220+
221+ # Test with response but no WWW-Authenticate (fallback)
222+ init_response = httpx .Response (
223+ status_code = 401 ,
224+ headers = {},
225+ request = httpx .Request ("GET" , "https://request-api.example.com" )
226+ )
227+
228+ request = await provider ._discover_protected_resource (init_response )
229+ assert request .method == "GET"
230+ assert str (request .url ) == "https://api.example.com/.well-known/oauth-protected-resource"
231+ assert "mcp-protocol-version" in request .headers
232+
233+ # Test with WWW-Authenticate header
234+ init_response .headers ["WWW-Authenticate" ] = 'Bearer resource_metadata="https://prm.example.com/.well-known/oauth-protected-resource/path"'
235+
236+ request = await provider ._discover_protected_resource (init_response )
237+ assert request .method == "GET"
238+ assert str (request .url ) == "https://prm.example.com/.well-known/oauth-protected-resource/path"
239+ assert "mcp-protocol-version" in request .headers
206240
207241 @pytest .mark .anyio
208242 async def test_discover_oauth_metadata_request (self , oauth_provider ):
@@ -544,3 +578,108 @@ async def test_auth_flow_with_valid_tokens(self, oauth_provider, mock_storage, v
544578 await auth_flow .asend (response )
545579 except StopAsyncIteration :
546580 pass # Expected
581+
582+
583+ class TestRFC9728WWWAuthenticate :
584+ """Test RFC9728 WWW-Authenticate header parsing functionality."""
585+
586+ @pytest .mark .parametrize ("www_auth_header,expected_url" , [
587+ # Quoted URL
588+ ('Bearer resource_metadata="https://api.example.com/.well-known/oauth-protected-resource"' ,
589+ "https://api.example.com/.well-known/oauth-protected-resource" ),
590+ # Unquoted URL
591+ ("Bearer resource_metadata=https://api.example.com/.well-known/oauth-protected-resource" ,
592+ "https://api.example.com/.well-known/oauth-protected-resource" ),
593+ # Complex header with multiple parameters
594+ ('Bearer realm="api", resource_metadata="https://api.example.com/.well-known/oauth-protected-resource", error="insufficient_scope"' ,
595+ "https://api.example.com/.well-known/oauth-protected-resource" ),
596+ # Different URL format
597+ ('Bearer resource_metadata="https://custom.domain.com/metadata"' ,
598+ "https://custom.domain.com/metadata" ),
599+ # With path and query params
600+ ('Bearer resource_metadata="https://api.example.com/auth/metadata?version=1"' ,
601+ "https://api.example.com/auth/metadata?version=1" ),
602+ ])
603+ def test_extract_resource_metadata_from_www_auth_valid_cases (self , client_metadata , mock_storage , www_auth_header , expected_url ):
604+ """Test extraction of resource_metadata URL from various valid WWW-Authenticate headers."""
605+ async def redirect_handler (url : str ) -> None :
606+ pass
607+
608+ async def callback_handler () -> tuple [str , str | None ]:
609+ return "test_auth_code" , "test_state"
610+
611+ provider = OAuthClientProvider (
612+ server_url = "https://api.example.com/v1/mcp" ,
613+ client_metadata = client_metadata ,
614+ storage = mock_storage ,
615+ redirect_handler = redirect_handler ,
616+ callback_handler = callback_handler ,
617+ )
618+
619+ init_response = httpx .Response (
620+ status_code = 401 ,
621+ headers = {"WWW-Authenticate" : www_auth_header },
622+ request = httpx .Request ("GET" , "https://api.example.com/test" )
623+ )
624+
625+ result = provider ._extract_resource_metadata_from_www_auth (init_response )
626+ assert result == expected_url
627+
628+ @pytest .mark .parametrize ("status_code,www_auth_header,description" , [
629+ # No header
630+ (401 , None , "no WWW-Authenticate header" ),
631+ # Empty header
632+ (401 , "" , "empty WWW-Authenticate header" ),
633+ # Header without resource_metadata
634+ (401 , 'Bearer realm="api", error="insufficient_scope"' , "no resource_metadata parameter" ),
635+ # Malformed header
636+ (401 , "Bearer resource_metadata=" , "malformed resource_metadata parameter" ),
637+ # Non-401 status code
638+ (200 , 'Bearer resource_metadata="https://api.example.com/.well-known/oauth-protected-resource"' , "200 OK response" ),
639+ (500 , 'Bearer resource_metadata="https://api.example.com/.well-known/oauth-protected-resource"' , "500 error response" ),
640+ ])
641+ def test_extract_resource_metadata_from_www_auth_invalid_cases (self , client_metadata , mock_storage , status_code , www_auth_header , description ):
642+ """Test extraction returns None for invalid cases."""
643+ async def redirect_handler (url : str ) -> None :
644+ pass
645+
646+ async def callback_handler () -> tuple [str , str | None ]:
647+ return "test_auth_code" , "test_state"
648+
649+ provider = OAuthClientProvider (
650+ server_url = "https://api.example.com/v1/mcp" ,
651+ client_metadata = client_metadata ,
652+ storage = mock_storage ,
653+ redirect_handler = redirect_handler ,
654+ callback_handler = callback_handler ,
655+ )
656+
657+ headers = {"WWW-Authenticate" : www_auth_header } if www_auth_header is not None else {}
658+ init_response = httpx .Response (
659+ status_code = status_code ,
660+ headers = headers ,
661+ request = httpx .Request ("GET" , "https://api.example.com/test" )
662+ )
663+
664+ result = provider ._extract_resource_metadata_from_www_auth (init_response )
665+ assert result is None , f"Should return None for { description } "
666+
667+ def test_extract_resource_metadata_from_www_auth_none_response (self , client_metadata , mock_storage ):
668+ """Test extraction with None response returns None."""
669+ async def redirect_handler (url : str ) -> None :
670+ pass
671+
672+ async def callback_handler () -> tuple [str , str | None ]:
673+ return "test_auth_code" , "test_state"
674+
675+ provider = OAuthClientProvider (
676+ server_url = "https://api.example.com/v1/mcp" ,
677+ client_metadata = client_metadata ,
678+ storage = mock_storage ,
679+ redirect_handler = redirect_handler ,
680+ callback_handler = callback_handler ,
681+ )
682+
683+ result = provider ._extract_resource_metadata_from_www_auth (None )
684+ assert result is None
685+
0 commit comments