Skip to content
Open
5 changes: 3 additions & 2 deletions src/mcp/client/auth/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
26 changes: 26 additions & 0 deletions tests/client/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"',
Expand Down Expand Up @@ -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"),
Expand All @@ -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."""
Expand Down
Loading