-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Fix substring matches in WWW-Authenticate parsing #3041
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -16,6 +16,52 @@ | |||||||||
| from mcp.shared.inbound import MCP_PROTOCOL_VERSION_HEADER | ||||||||||
|
|
||||||||||
|
|
||||||||||
| def _split_www_authenticate_segments(header_value: str) -> list[str]: | ||||||||||
| """Split a WWW-Authenticate header on top-level commas.""" | ||||||||||
| segments: list[str] = [] | ||||||||||
| current: list[str] = [] | ||||||||||
| in_quotes = False | ||||||||||
|
|
||||||||||
| for char in header_value: | ||||||||||
| if char == '"': | ||||||||||
| in_quotes = not in_quotes | ||||||||||
| if char == "," and not in_quotes: | ||||||||||
| segment = "".join(current).strip() | ||||||||||
| if segment: | ||||||||||
| segments.append(segment) | ||||||||||
| current = [] | ||||||||||
| continue | ||||||||||
| current.append(char) | ||||||||||
|
|
||||||||||
| tail = "".join(current).strip() | ||||||||||
| if tail: | ||||||||||
| segments.append(tail) | ||||||||||
| return segments | ||||||||||
|
|
||||||||||
|
|
||||||||||
| def _extract_bearer_auth_params(www_auth_header: str) -> str | None: | ||||||||||
| """Return the auth-param portion of the first Bearer challenge.""" | ||||||||||
| segments = _split_www_authenticate_segments(www_auth_header) | ||||||||||
| collecting = False | ||||||||||
| auth_params: list[str] = [] | ||||||||||
|
|
||||||||||
| for segment in segments: | ||||||||||
| scheme, separator, remainder = segment.partition(" ") | ||||||||||
| if scheme.lower() == "bearer" and separator: | ||||||||||
| collecting = True | ||||||||||
| auth_params = [remainder.strip()] | ||||||||||
| continue | ||||||||||
|
|
||||||||||
| if collecting: | ||||||||||
| if separator and "=" not in scheme: | ||||||||||
| break | ||||||||||
|
Comment on lines
+56
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Bearer params after a valid Prompt for AI agents
Suggested change
|
||||||||||
| auth_params.append(segment) | ||||||||||
|
|
||||||||||
| if not auth_params: | ||||||||||
| return None | ||||||||||
| return ", ".join(part for part in auth_params if part) | ||||||||||
|
|
||||||||||
|
|
||||||||||
| def extract_field_from_www_auth(response: Response, field_name: str) -> str | None: | ||||||||||
| """Extract field from WWW-Authenticate header. | ||||||||||
|
|
||||||||||
|
|
@@ -26,13 +72,16 @@ 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,]+))' | ||||||||||
| match = re.search(pattern, www_auth_header) | ||||||||||
| auth_params = _extract_bearer_auth_params(www_auth_header) | ||||||||||
| if auth_params is None: | ||||||||||
| return None | ||||||||||
|
|
||||||||||
| if match: | ||||||||||
| # Return quoted value if present, otherwise unquoted value | ||||||||||
| return match.group(1) or match.group(2) | ||||||||||
| # Match comma-delimited auth-params while respecting quoted values. | ||||||||||
| pattern = re.compile(r'(?:^|,\s*)(?P<name>[A-Za-z][A-Za-z0-9_-]*)=(?:"(?P<quoted>[^"]+)"|(?P<unquoted>[^,\s]+))') | ||||||||||
| for match in pattern.finditer(auth_params): | ||||||||||
| if match.group("name") == field_name: | ||||||||||
| # Return quoted value if present, otherwise unquoted value | ||||||||||
| return match.group("quoted") or match.group("unquoted") | ||||||||||
|
|
||||||||||
| return None | ||||||||||
|
|
||||||||||
|
|
||||||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: When a
WWW-Authenticateheader contains more than one Bearer challenge, the parser currently keeps overwriting the collected auth params and ends up using the last Bearer challenge. That can make scope/resource metadata extraction come from a different challenge than intended. The overwrite happens because everyBearer ...segment reinitializesauth_params; stopping at the first Bearer challenge (or only initializing once) would keep behavior consistent with the function contract.Prompt for AI agents