fix: pass conformance auth scenarios, add RFC 8707 resource validation (#2010)
This commit is contained in:
parent
7c7c13b648
commit
239d682b6f
1 changed files with 187 additions and 8 deletions
|
|
@ -229,6 +229,7 @@ class OAuthClientProvider(httpx.Auth):
|
|||
callback_handler: Callable[[], Awaitable[tuple[str, str | None]]] | None = None,
|
||||
timeout: float = 300.0,
|
||||
client_metadata_url: str | None = None,
|
||||
validate_resource_url: Callable[[str, str | None], Awaitable[None]] | None = None,
|
||||
):
|
||||
"""Initialize OAuth2 authentication.
|
||||
|
||||
|
|
@ -243,6 +244,10 @@ class OAuthClientProvider(httpx.Auth):
|
|||
advertises client_id_metadata_document_supported=true, this URL will be
|
||||
used as the client_id instead of performing dynamic client registration.
|
||||
Must be a valid HTTPS URL with a non-root pathname.
|
||||
validate_resource_url: Optional callback to override resource URL validation.
|
||||
Called with (server_url, prm_resource) where prm_resource is the resource
|
||||
from Protected Resource Metadata (or None if not present). If not provided,
|
||||
default validation rejects mismatched resources per RFC 8707.
|
||||
|
||||
Raises:
|
||||
ValueError: If client_metadata_url is provided but not a valid HTTPS URL
|
||||
|
|
@ -263,6 +268,7 @@ class OAuthClientProvider(httpx.Auth):
|
|||
timeout=timeout,
|
||||
client_metadata_url=client_metadata_url,
|
||||
)
|
||||
self._validate_resource_url_callback = validate_resource_url
|
||||
self._initialized = False
|
||||
|
||||
async def _handle_protected_resource_response(self, response: httpx.Response) -> bool:
|
||||
|
|
@ -476,6 +482,26 @@ class OAuthClientProvider(httpx.Auth):
|
|||
metadata = OAuthMetadata.model_validate_json(content)
|
||||
self.context.oauth_metadata = metadata
|
||||
|
||||
async def _validate_resource_match(self, prm: ProtectedResourceMetadata) -> None:
|
||||
"""Validate that PRM resource matches the server URL per RFC 8707."""
|
||||
prm_resource = str(prm.resource) if prm.resource else None
|
||||
|
||||
if self._validate_resource_url_callback is not None:
|
||||
await self._validate_resource_url_callback(self.context.server_url, prm_resource)
|
||||
return
|
||||
|
||||
if not prm_resource:
|
||||
return # pragma: no cover
|
||||
default_resource = resource_url_from_server_url(self.context.server_url)
|
||||
# Normalize: Pydantic AnyHttpUrl adds trailing slash to root URLs
|
||||
# (e.g. "https://example.com/") while resource_url_from_server_url may not.
|
||||
if not default_resource.endswith("/"):
|
||||
default_resource += "/"
|
||||
if not prm_resource.endswith("/"):
|
||||
prm_resource += "/"
|
||||
if not check_resource_allowed(requested_resource=default_resource, configured_resource=prm_resource):
|
||||
raise OAuthFlowError(f"Protected resource {prm_resource} does not match expected {default_resource}")
|
||||
|
||||
async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.Request, httpx.Response]:
|
||||
"""HTTPX auth flow integration."""
|
||||
async with self.context.lock:
|
||||
|
|
@ -517,6 +543,8 @@ class OAuthClientProvider(httpx.Auth):
|
|||
|
||||
prm = await handle_protected_resource_response(discovery_response)
|
||||
if prm:
|
||||
# Validate PRM resource matches server URL (RFC 8707)
|
||||
await self._validate_resource_match(prm)
|
||||
self.context.protected_resource_metadata = prm
|
||||
|
||||
# todo: try all authorization_servers to find the OASM
|
||||
|
|
|
|||
Loading…
Reference in a new issue