Preserve empty URL paths on OAuth metadata models (#2925)
This commit is contained in:
parent
5a3412ddc1
commit
b7a5bffed0
3 changed files with 19 additions and 3 deletions
|
|
@ -1220,6 +1220,16 @@ Tasks are expected to return as a separate MCP extension in a future release.
|
||||||
|
|
||||||
## Bug Fixes
|
## Bug Fixes
|
||||||
|
|
||||||
|
### OAuth metadata URLs no longer gain a trailing slash
|
||||||
|
|
||||||
|
`OAuthMetadata`, `ProtectedResourceMetadata`, and `OAuthClientMetadata` now set
|
||||||
|
`url_preserve_empty_path=True` (Pydantic 2.12+). A path-less URL parsed from the wire keeps its
|
||||||
|
empty path instead of acquiring a trailing slash, so e.g. an `issuer` of `https://as.example.com`
|
||||||
|
round-trips as `https://as.example.com` rather than `https://as.example.com/`. This matters for
|
||||||
|
RFC 9207 / RFC 8414 issuer comparisons, which require simple string comparison (RFC 3986 §6.2.1).
|
||||||
|
URLs constructed in Python from an already-built `AnyHttpUrl` object are unaffected (they were
|
||||||
|
normalized at construction); only values parsed from strings/JSON change.
|
||||||
|
|
||||||
### Lowlevel `Server`: `subscribe` capability now correctly reported
|
### Lowlevel `Server`: `subscribe` capability now correctly reported
|
||||||
|
|
||||||
Previously, the lowlevel `Server` hardcoded `subscribe=False` in resource capabilities even when a `subscribe_resource()` handler was registered. The `subscribe` capability is now dynamically set to `True` when an `on_subscribe_resource` handler is provided. Clients that previously didn't see `subscribe: true` in capabilities will now see it when a handler is registered, which may change client behavior.
|
Previously, the lowlevel `Server` hardcoded `subscribe=False` in resource capabilities even when a `subscribe_resource()` handler was registered. The `subscribe` capability is now dynamically set to `True` when an `on_subscribe_resource` handler is provided. Clients that previously didn't see `subscribe: true` in capabilities will now see it when a handler is registered, which may change client behavior.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
from pydantic import AnyHttpUrl, AnyUrl, BaseModel, Field, field_validator
|
from pydantic import AnyHttpUrl, AnyUrl, BaseModel, ConfigDict, Field, field_validator
|
||||||
|
|
||||||
|
|
||||||
class OAuthToken(BaseModel):
|
class OAuthToken(BaseModel):
|
||||||
|
|
@ -37,6 +37,8 @@ class OAuthClientMetadata(BaseModel):
|
||||||
See https://datatracker.ietf.org/doc/html/rfc7591#section-2
|
See https://datatracker.ietf.org/doc/html/rfc7591#section-2
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(url_preserve_empty_path=True)
|
||||||
|
|
||||||
redirect_uris: list[AnyUrl] | None = Field(..., min_length=1)
|
redirect_uris: list[AnyUrl] | None = Field(..., min_length=1)
|
||||||
# supported auth methods for the token endpoint
|
# supported auth methods for the token endpoint
|
||||||
token_endpoint_auth_method: (
|
token_endpoint_auth_method: (
|
||||||
|
|
@ -123,6 +125,8 @@ class OAuthMetadata(BaseModel):
|
||||||
See https://datatracker.ietf.org/doc/html/rfc8414#section-2
|
See https://datatracker.ietf.org/doc/html/rfc8414#section-2
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(url_preserve_empty_path=True)
|
||||||
|
|
||||||
issuer: AnyHttpUrl
|
issuer: AnyHttpUrl
|
||||||
authorization_endpoint: AnyHttpUrl
|
authorization_endpoint: AnyHttpUrl
|
||||||
token_endpoint: AnyHttpUrl
|
token_endpoint: AnyHttpUrl
|
||||||
|
|
@ -152,6 +156,8 @@ class ProtectedResourceMetadata(BaseModel):
|
||||||
See https://datatracker.ietf.org/doc/html/rfc9728#section-2
|
See https://datatracker.ietf.org/doc/html/rfc9728#section-2
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(url_preserve_empty_path=True)
|
||||||
|
|
||||||
resource: AnyHttpUrl
|
resource: AnyHttpUrl
|
||||||
authorization_servers: list[AnyHttpUrl] = Field(..., min_length=1)
|
authorization_servers: list[AnyHttpUrl] = Field(..., min_length=1)
|
||||||
jwks_uri: AnyHttpUrl | None = None
|
jwks_uri: AnyHttpUrl | None = None
|
||||||
|
|
|
||||||
|
|
@ -517,10 +517,10 @@ class TestOAuthFallback:
|
||||||
}"""
|
}"""
|
||||||
response = httpx.Response(200, content=content)
|
response = httpx.Response(200, content=content)
|
||||||
|
|
||||||
# Should set metadata
|
# Should set metadata; the empty path is preserved (no trailing slash added)
|
||||||
await oauth_provider._handle_oauth_metadata_response(response)
|
await oauth_provider._handle_oauth_metadata_response(response)
|
||||||
assert oauth_provider.context.oauth_metadata is not None
|
assert oauth_provider.context.oauth_metadata is not None
|
||||||
assert str(oauth_provider.context.oauth_metadata.issuer) == "https://auth.example.com/"
|
assert str(oauth_provider.context.oauth_metadata.issuer) == "https://auth.example.com"
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_prioritize_www_auth_scope_over_prm(
|
async def test_prioritize_www_auth_scope_over_prm(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue