Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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