Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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