Skip to content

Commit 3e99ffe

Browse files
committed
Handle more Bearer auth-param edge cases
1 parent d725212 commit 3e99ffe

2 files changed

Lines changed: 39 additions & 2 deletions

File tree

src/mcp/client/auth/utils.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,14 @@ def _extract_bearer_auth_params(www_auth_header: str) -> str | None:
4848
for segment in segments:
4949
scheme, separator, remainder = segment.partition(" ")
5050
if scheme.lower() == "bearer" and separator:
51+
if collecting:
52+
break
5153
collecting = True
5254
auth_params = [remainder.strip()]
5355
continue
5456

5557
if collecting:
56-
if separator and "=" not in scheme:
58+
if separator and "=" not in scheme and not remainder.lstrip().startswith("="):
5759
break
5860
auth_params.append(segment)
5961

@@ -77,7 +79,9 @@ def extract_field_from_www_auth(response: Response, field_name: str) -> str | No
7779
return None
7880

7981
# Match comma-delimited auth-params while respecting quoted values.
80-
pattern = re.compile(r'(?:^|,\s*)(?P<name>[A-Za-z][A-Za-z0-9_-]*)=(?:"(?P<quoted>[^"]+)"|(?P<unquoted>[^,\s]+))')
82+
pattern = re.compile(
83+
r'(?:^|,\s*)(?P<name>[A-Za-z][A-Za-z0-9_-]*)\s*=\s*(?:"(?P<quoted>[^"]*)"|(?P<unquoted>[^,\s]+))'
84+
)
8185
for match in pattern.finditer(auth_params):
8286
if match.group("name") == field_name:
8387
# Return quoted value if present, otherwise unquoted value

tests/client/test_auth.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,39 @@ def test_pkce_uniqueness(self):
181181
assert pkce1.code_challenge != pkce2.code_challenge
182182

183183

184+
class TestExtractFieldFromWwwAuth:
185+
def test_uses_first_bearer_challenge_when_multiple_are_present(self):
186+
response = httpx.Response(
187+
401,
188+
headers={
189+
"WWW-Authenticate": (
190+
'Basic realm="legacy", Bearer scope="read", error="insufficient_scope", Bearer scope="write"'
191+
)
192+
},
193+
request=httpx.Request("GET", "https://example.com"),
194+
)
195+
196+
assert extract_scope_from_www_auth(response) == "read"
197+
198+
def test_supports_optional_whitespace_around_equals(self):
199+
response = httpx.Response(
200+
401,
201+
headers={
202+
"WWW-Authenticate": (
203+
'Bearer error="insufficient_scope", scope = "read write", '
204+
'resource_metadata = "https://example.com/.well-known/oauth-protected-resource"'
205+
)
206+
},
207+
request=httpx.Request("GET", "https://example.com"),
208+
)
209+
210+
assert extract_scope_from_www_auth(response) == "read write"
211+
assert (
212+
extract_resource_metadata_from_www_auth(response)
213+
== "https://example.com/.well-known/oauth-protected-resource"
214+
)
215+
216+
184217
class TestOAuthContext:
185218
"""Test OAuth context functionality."""
186219

0 commit comments

Comments
 (0)