-
Notifications
You must be signed in to change notification settings - Fork 3.3k
fix(auth): coerce empty-string optional URL fields to None in OAuthClientMetadata #2404
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
Merged
+102
−2
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
fef29a5
fix(auth): coerce empty-string optional URL fields to None in OAuthCl…
felixweinberger b0c83a8
test: move OAuthClientMetadata empty-URL tests to tests/shared, drop …
felixweinberger bfc7da3
docs: clarify Test-class convention applies even in legacy files
felixweinberger File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
🟣 This PR correctly fixes empty-string coercion for OAuthClientMetadata, but OAuthMetadata (RFC 8414) and ProtectedResourceMetadata (RFC 9728) in the same file have the identical failure mode for their optional URL fields — a pre-existing gap now made visible by the inconsistency. The same non-compliant servers that echo "" in DCR responses could equally send "" in their /.well-known/oauth-authorization-server or protected resource metadata responses, causing ValidationError when parsing those server-returned payloads.
Extended reasoning...
What the bug is and how it manifests
The PR correctly identifies that some real-world OAuth servers echo omitted optional URL fields as empty strings rather than dropping the keys, and that Pydantic's AnyHttpUrl rejects "" — causing ValidationError at parse time. The fix (a before-mode field_validator that maps "" → None) is applied to OAuthClientMetadata, but two sibling models in the same file are left unpatched:
The specific code path that triggers it
When the MCP client fetches /.well-known/oauth-authorization-server (or equivalent), the response JSON is parsed via OAuthMetadata.model_validate(). If the server returns {"issuer": "https://example.com", "authorization_endpoint": "...", "token_endpoint": "...", "registration_endpoint": ""}, Pydantic's AnyHttpUrl validator fires on registration_endpoint before any field_validator runs (there is none), sees an empty string, and raises ValidationError — even though registration_endpoint is marked OPTIONAL in RFC 8414 §2. The same applies to the ProtectedResourceMetadata parse at the protected resource metadata endpoint.
Why existing code doesn't prevent it
OAuthMetadata and ProtectedResourceMetadata have no field_validator at all for their optional AnyHttpUrl fields. The PR adds one only to OAuthClientMetadata. Pydantic's default behavior for AnyHttpUrl | None fields is to accept None or a valid URL; an empty string is neither, so it raises. There is no upstream fallback.
Impact
Any MCP client talking to a non-compliant-but-widely-deployed authorization server that echoes "" for omitted AS-metadata or resource-metadata fields will fail during the discovery phase — before client registration even begins. This is a harder failure than the DCR failure fixed by the PR, because it blocks all OAuth flows, not just re-registration.
How to fix it
Apply the same field_validator pattern used for OAuthClientMetadata to OAuthMetadata (covering registration_endpoint, service_documentation, op_policy_uri, op_tos_uri, revocation_endpoint, introspection_endpoint) and to ProtectedResourceMetadata (covering jwks_uri, resource_documentation, resource_policy_uri, resource_tos_uri).
Step-by-step proof
Addressing the refutation
The refuting verifier correctly notes the PR does not introduce this bug — it pre-dates the PR. However, this PR directly modifies auth.py and adds the exact fix pattern needed. The PR's description explicitly cites Postel's law and real-world non-compliant servers as motivation. Those same servers' AS metadata and resource metadata endpoints are equally likely to echo "" for optional fields. The code review is the appropriate venue to flag that the fix was applied incompletely across the three related models in the same file.