@@ -198,6 +198,7 @@ class TestOAuthFlow:
198198 @pytest .mark .anyio
199199 async def test_discover_protected_resource_request (self , client_metadata , mock_storage ):
200200 """Test protected resource discovery request building maintains backward compatibility."""
201+
201202 async def redirect_handler (url : str ) -> None :
202203 pass
203204
@@ -211,30 +212,24 @@ async def callback_handler() -> tuple[str, str | None]:
211212 redirect_handler = redirect_handler ,
212213 callback_handler = callback_handler ,
213214 )
214-
215- # Test without response (backward compatibility)
216- request = await provider ._discover_protected_resource ()
217- assert request .method == "GET"
218- assert str (request .url ) == "https://api.example.com/.well-known/oauth-protected-resource"
219- assert "mcp-protocol-version" in request .headers
220-
221- # Test with response but no WWW-Authenticate (fallback)
215+
216+ # Test without WWW-Authenticate (fallback)
222217 init_response = httpx .Response (
223- status_code = 401 ,
224- headers = {},
225- request = httpx .Request ("GET" , "https://request-api.example.com" )
218+ status_code = 401 , headers = {}, request = httpx .Request ("GET" , "https://request-api.example.com" )
226219 )
227220
228221 request = await provider ._discover_protected_resource (init_response )
229- assert request .method == "GET"
222+ assert request .method == "GET"
230223 assert str (request .url ) == "https://api.example.com/.well-known/oauth-protected-resource"
231224 assert "mcp-protocol-version" in request .headers
232225
233226 # 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-
227+ init_response .headers ["WWW-Authenticate" ] = (
228+ 'Bearer resource_metadata="https://prm.example.com/.well-known/oauth-protected-resource/path"'
229+ )
230+
236231 request = await provider ._discover_protected_resource (init_response )
237- assert request .method == "GET"
232+ assert request .method == "GET"
238233 assert str (request .url ) == "https://prm.example.com/.well-known/oauth-protected-resource/path"
239234 assert "mcp-protocol-version" in request .headers
240235
@@ -580,28 +575,42 @@ async def test_auth_flow_with_valid_tokens(self, oauth_provider, mock_storage, v
580575 pass # Expected
581576
582577
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 ):
578+ class TestProtectedResourceWWWAuthenticate :
579+ """Test RFC9728 WWW-Authenticate header parsing functionality for protected resource."""
580+
581+ @pytest .mark .parametrize (
582+ "www_auth_header,expected_url" ,
583+ [
584+ # Quoted URL
585+ (
586+ 'Bearer resource_metadata="https://api.example.com/.well-known/oauth-protected-resource"' ,
587+ "https://api.example.com/.well-known/oauth-protected-resource" ,
588+ ),
589+ # Unquoted URL
590+ (
591+ "Bearer resource_metadata=https://api.example.com/.well-known/oauth-protected-resource" ,
592+ "https://api.example.com/.well-known/oauth-protected-resource" ,
593+ ),
594+ # Complex header with multiple parameters
595+ (
596+ 'Bearer realm="api", resource_metadata="https://api.example.com/.well-known/oauth-protected-resource", '
597+ 'error="insufficient_scope"' ,
598+ "https://api.example.com/.well-known/oauth-protected-resource" ,
599+ ),
600+ # Different URL format
601+ ('Bearer resource_metadata="https://custom.domain.com/metadata"' , "https://custom.domain.com/metadata" ),
602+ # With path and query params
603+ (
604+ 'Bearer resource_metadata="https://api.example.com/auth/metadata?version=1"' ,
605+ "https://api.example.com/auth/metadata?version=1" ,
606+ ),
607+ ],
608+ )
609+ def test_extract_resource_metadata_from_www_auth_valid_cases (
610+ self , client_metadata , mock_storage , www_auth_header , expected_url
611+ ):
604612 """Test extraction of resource_metadata URL from various valid WWW-Authenticate headers."""
613+
605614 async def redirect_handler (url : str ) -> None :
606615 pass
607616
@@ -615,31 +624,45 @@ async def callback_handler() -> tuple[str, str | None]:
615624 redirect_handler = redirect_handler ,
616625 callback_handler = callback_handler ,
617626 )
618-
627+
619628 init_response = httpx .Response (
620629 status_code = 401 ,
621630 headers = {"WWW-Authenticate" : www_auth_header },
622- request = httpx .Request ("GET" , "https://api.example.com/test" )
631+ request = httpx .Request ("GET" , "https://api.example.com/test" ),
623632 )
624-
633+
625634 result = provider ._extract_resource_metadata_from_www_auth (init_response )
626635 assert result == expected_url
627636
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 ):
637+ @pytest .mark .parametrize (
638+ "status_code,www_auth_header,description" ,
639+ [
640+ # No header
641+ (401 , None , "no WWW-Authenticate header" ),
642+ # Empty header
643+ (401 , "" , "empty WWW-Authenticate header" ),
644+ # Header without resource_metadata
645+ (401 , 'Bearer realm="api", error="insufficient_scope"' , "no resource_metadata parameter" ),
646+ # Malformed header
647+ (401 , "Bearer resource_metadata=" , "malformed resource_metadata parameter" ),
648+ # Non-401 status code
649+ (
650+ 200 ,
651+ 'Bearer resource_metadata="https://api.example.com/.well-known/oauth-protected-resource"' ,
652+ "200 OK response" ,
653+ ),
654+ (
655+ 500 ,
656+ 'Bearer resource_metadata="https://api.example.com/.well-known/oauth-protected-resource"' ,
657+ "500 error response" ,
658+ ),
659+ ],
660+ )
661+ def test_extract_resource_metadata_from_www_auth_invalid_cases (
662+ self , client_metadata , mock_storage , status_code , www_auth_header , description
663+ ):
642664 """Test extraction returns None for invalid cases."""
665+
643666 async def redirect_handler (url : str ) -> None :
644667 pass
645668
@@ -653,33 +676,11 @@ async def callback_handler() -> tuple[str, str | None]:
653676 redirect_handler = redirect_handler ,
654677 callback_handler = callback_handler ,
655678 )
656-
679+
657680 headers = {"WWW-Authenticate" : www_auth_header } if www_auth_header is not None else {}
658681 init_response = httpx .Response (
659- status_code = status_code ,
660- headers = headers ,
661- request = httpx .Request ("GET" , "https://api.example.com/test" )
682+ status_code = status_code , headers = headers , request = httpx .Request ("GET" , "https://api.example.com/test" )
662683 )
663-
684+
664685 result = provider ._extract_resource_metadata_from_www_auth (init_response )
665686 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