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 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:
Expand Down
48 changes: 48 additions & 0 deletions tests/client/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
Loading