From 5ce7010ba43f9936d868a872aa3159d879c0be44 Mon Sep 17 00:00:00 2001 From: Pradeep Ramola Date: Wed, 1 Jul 2026 21:33:21 -0400 Subject: [PATCH] fix: match complete www-authenticate auth params --- src/mcp/client/auth/utils.py | 5 +++-- tests/client/test_auth.py | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/mcp/client/auth/utils.py b/src/mcp/client/auth/utils.py index d6b05e066..c0df44f10 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 complete auth-param names: field_name="value" or field_name=value (unquoted). + field_pattern = re.escape(field_name) + pattern = rf'(?:^|[\s,]){field_pattern}=(?:"([^"]+)"|([^\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..365612f25 100644 --- a/tests/client/test_auth.py +++ b/tests/client/test_auth.py @@ -2005,6 +2005,14 @@ class TestWWWAuthenticate: ), # Multiple parameters with unquoted value ('Bearer realm="api", scope=basic', "scope", "basic"), + # Decoy parameter name before the real field + ('Bearer error_scope="decoy", scope="read write"', "scope", "read write"), + ( + 'Bearer x_resource_metadata="https://decoy.example.com", ' + 'resource_metadata="https://api.example.com/.well-known/oauth-protected-resource"', + "resource_metadata", + "https://api.example.com/.well-known/oauth-protected-resource", + ), # Values with special characters ( 'Bearer scope="resource:read resource:write user_profile"', @@ -2047,6 +2055,12 @@ def test_extract_field_from_www_auth_valid_cases( # Header without requested field ('Bearer realm="api", error="insufficient_scope"', "scope", "no scope parameter"), ('Bearer realm="api", scope="read write"', "resource_metadata", "no resource_metadata parameter"), + ('Bearer custom_scope="leaked"', "scope", "substring param name should not match scope"), + ( + 'Bearer x_resource_metadata="https://decoy.example.com"', + "resource_metadata", + "substring param name should not match resource_metadata", + ), # Malformed field (empty value) ("Bearer scope=", "scope", "malformed scope parameter"), ("Bearer resource_metadata=", "resource_metadata", "malformed resource_metadata parameter"), @@ -2070,6 +2084,18 @@ 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_resource_metadata_from_www_auth_ignores_substring_param_name(self): + """Test resource_metadata extraction ignores auth-params with longer names.""" + 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."""