From b8b600172999f08d67c763b3d44e7597605779ac Mon Sep 17 00:00:00 2001 From: tarunag10 Date: Sun, 28 Jun 2026 13:03:07 +0530 Subject: [PATCH] fix(auth): match complete WWW-Authenticate params --- src/mcp/client/auth/utils.py | 5 ++-- tests/client/test_auth.py | 48 ++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/mcp/client/auth/utils.py b/src/mcp/client/auth/utils.py index d6b05e066..8fa238b1b 100644 --- a/src/mcp/client/auth/utils.py +++ b/src/mcp/client/auth/utils.py @@ -26,8 +26,9 @@ def extract_field_from_www_auth(response: Response, field_name: str) -> str | No if not www_auth_header: return None - # Pattern matches: field_name="value" or field_name=value (unquoted) - pattern = rf'{field_name}=(?:"([^"]+)"|([^\s,]+))' + # Pattern matches a complete auth-param name, not a suffix of another + # parameter such as error_scope or x_resource_metadata. + pattern = rf'(?:^|[\s,]){re.escape(field_name)}=(?:"([^"]+)"|([^\s,]+))' match = re.search(pattern, www_auth_header) if match: diff --git a/tests/client/test_auth.py b/tests/client/test_auth.py index 1ec38ccf6..547d99fa3 100644 --- a/tests/client/test_auth.py +++ b/tests/client/test_auth.py @@ -2070,6 +2070,54 @@ def test_extract_field_from_www_auth_invalid_cases( result = extract_field_from_www_auth(init_response, field_name) assert result is None, f"Should return None for {description}" + def test_extract_field_from_www_auth_does_not_match_substring_param_name( + self, + client_metadata: OAuthClientMetadata, + mock_storage: MockTokenStorage, + ): + """Test auth-param names are matched exactly, not as substrings.""" + + init_response = httpx.Response( + status_code=401, + headers={"WWW-Authenticate": 'Bearer error_scope="decoy", scope="read write"'}, + request=httpx.Request("GET", "https://api.example.com/test"), + ) + + result = extract_field_from_www_auth(init_response, "scope") + assert result == "read write" + + def test_extract_field_from_www_auth_ignores_prefixed_param_only( + self, + client_metadata: OAuthClientMetadata, + mock_storage: MockTokenStorage, + ): + """Test a prefixed auth-param does not satisfy the requested field.""" + + init_response = httpx.Response( + status_code=401, + headers={"WWW-Authenticate": 'Bearer custom_scope="leaked"'}, + request=httpx.Request("GET", "https://api.example.com/test"), + ) + + result = extract_field_from_www_auth(init_response, "scope") + assert result is None + + def test_extract_resource_metadata_from_www_auth_ignores_prefixed_param( + self, + client_metadata: OAuthClientMetadata, + mock_storage: MockTokenStorage, + ): + """Test resource_metadata does not match inside another auth-param name.""" + + init_response = httpx.Response( + status_code=401, + headers={"WWW-Authenticate": 'Bearer x_resource_metadata="https://decoy.example.com"'}, + request=httpx.Request("GET", "https://api.example.com/test"), + ) + + result = extract_resource_metadata_from_www_auth(init_response) + assert result is None + class TestCIMD: """Test Client ID Metadata Document (CIMD) support."""