Rename scopes= to scope= on the client-credentials OAuth providers (#3166)
This commit is contained in:
parent
5dd062d077
commit
e90a66b554
7 changed files with 34 additions and 18 deletions
|
|
@ -112,7 +112,7 @@ A nightly job, a CI step, another service. There is no browser and nobody to cli
|
||||||
What changed:
|
What changed:
|
||||||
|
|
||||||
* No `OAuthClientMetadata`, no handlers. You pass `client_id` and `client_secret`; the provider builds a minimal `client_credentials` registration around them and skips dynamic registration entirely.
|
* No `OAuthClientMetadata`, no handlers. You pass `client_id` and `client_secret`; the provider builds a minimal `client_credentials` registration around them and skips dynamic registration entirely.
|
||||||
* `scopes` is a space-separated string, the OAuth wire format.
|
* `scope` is a space-separated string, the OAuth wire format.
|
||||||
* Everything downstream is identical: the same `TokenStorage`, the same `httpx2.AsyncClient(auth=...)`, the same `streamable_http_client`.
|
* Everything downstream is identical: the same `TokenStorage`, the same `httpx2.AsyncClient(auth=...)`, the same `streamable_http_client`.
|
||||||
|
|
||||||
By default the secret travels as HTTP Basic auth on the token request (`client_secret_basic`). Pass `token_endpoint_auth_method="client_secret_post"` to put it in the form body instead. Some authorization servers only accept one of the two.
|
By default the secret travels as HTTP Basic auth on the token request (`client_secret_basic`). Pass `token_endpoint_auth_method="client_secret_post"` to put it in the form body instead. Some authorization servers only accept one of the two.
|
||||||
|
|
|
||||||
|
|
@ -1975,6 +1975,22 @@ async def callback_handler() -> AuthorizationCodeResult:
|
||||||
|
|
||||||
Forward the `iss` query parameter from the redirect so the validation can run: omitting it makes the flow fail with `OAuthFlowError` against servers that advertise `authorization_response_iss_parameter_supported`, and silently skips the check for servers that send `iss` without advertising it.
|
Forward the `iss` query parameter from the redirect so the validation can run: omitting it makes the flow fail with `OAuthFlowError` against servers that advertise `authorization_response_iss_parameter_supported`, and silently skips the check for servers that send `iss` without advertising it.
|
||||||
|
|
||||||
|
### `scopes=` renamed to `scope=` on the client-credentials providers
|
||||||
|
|
||||||
|
`ClientCredentialsOAuthProvider` and `PrivateKeyJWTOAuthProvider` took the requested scope as a keyword named `scopes`, even though the value is a single space-separated string, not a list. The parameter is now `scope`, matching the RFC 6749 wire parameter, `OAuthClientMetadata.scope`, and the newer `IdentityAssertionOAuthProvider`.
|
||||||
|
|
||||||
|
**Before (v1):**
|
||||||
|
|
||||||
|
```python
|
||||||
|
ClientCredentialsOAuthProvider(..., scopes="read write")
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (v2):**
|
||||||
|
|
||||||
|
```python
|
||||||
|
ClientCredentialsOAuthProvider(..., scope="read write")
|
||||||
|
```
|
||||||
|
|
||||||
### Client rejects authorization server metadata with a mismatched `issuer`
|
### Client rejects authorization server metadata with a mismatched `issuer`
|
||||||
|
|
||||||
During OAuth discovery, `OAuthClientProvider` now validates that the authorization server
|
During OAuth discovery, `OAuthClientProvider` now validates that the authorization server
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ oauth = ClientCredentialsOAuthProvider(
|
||||||
storage=InMemoryTokenStorage(),
|
storage=InMemoryTokenStorage(),
|
||||||
client_id="reporting-agent",
|
client_id="reporting-agent",
|
||||||
client_secret="...",
|
client_secret="...",
|
||||||
scopes="user",
|
scope="user",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ def build_auth(_http: httpx2.AsyncClient) -> httpx2.Auth:
|
||||||
storage=InMemoryTokenStorage(),
|
storage=InMemoryTokenStorage(),
|
||||||
client_id=DEMO_CLIENT_ID,
|
client_id=DEMO_CLIENT_ID,
|
||||||
client_secret=DEMO_CLIENT_SECRET,
|
client_secret=DEMO_CLIENT_SECRET,
|
||||||
scopes=DEMO_SCOPE,
|
scope=DEMO_SCOPE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ class ClientCredentialsOAuthProvider(OAuthClientProvider):
|
||||||
client_id: str,
|
client_id: str,
|
||||||
client_secret: str,
|
client_secret: str,
|
||||||
token_endpoint_auth_method: Literal["client_secret_basic", "client_secret_post"] = "client_secret_basic",
|
token_endpoint_auth_method: Literal["client_secret_basic", "client_secret_post"] = "client_secret_basic",
|
||||||
scopes: str | None = None,
|
scope: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize client_credentials OAuth provider.
|
"""Initialize client_credentials OAuth provider.
|
||||||
|
|
||||||
|
|
@ -57,14 +57,14 @@ class ClientCredentialsOAuthProvider(OAuthClientProvider):
|
||||||
client_secret: The OAuth client secret.
|
client_secret: The OAuth client secret.
|
||||||
token_endpoint_auth_method: Authentication method for token endpoint.
|
token_endpoint_auth_method: Authentication method for token endpoint.
|
||||||
Either "client_secret_basic" (default) or "client_secret_post".
|
Either "client_secret_basic" (default) or "client_secret_post".
|
||||||
scopes: Optional space-separated list of scopes to request.
|
scope: Optional space-separated list of scopes to request.
|
||||||
"""
|
"""
|
||||||
# Build minimal client_metadata for the base class
|
# Build minimal client_metadata for the base class
|
||||||
client_metadata = OAuthClientMetadata(
|
client_metadata = OAuthClientMetadata(
|
||||||
redirect_uris=None,
|
redirect_uris=None,
|
||||||
grant_types=["client_credentials"],
|
grant_types=["client_credentials"],
|
||||||
token_endpoint_auth_method=token_endpoint_auth_method,
|
token_endpoint_auth_method=token_endpoint_auth_method,
|
||||||
scope=scopes,
|
scope=scope,
|
||||||
)
|
)
|
||||||
super().__init__(server_url, client_metadata, storage, None, None, 300.0)
|
super().__init__(server_url, client_metadata, storage, None, None, 300.0)
|
||||||
# Store client_info to be set during _initialize - no dynamic registration needed
|
# Store client_info to be set during _initialize - no dynamic registration needed
|
||||||
|
|
@ -74,7 +74,7 @@ class ClientCredentialsOAuthProvider(OAuthClientProvider):
|
||||||
client_secret=client_secret,
|
client_secret=client_secret,
|
||||||
grant_types=["client_credentials"],
|
grant_types=["client_credentials"],
|
||||||
token_endpoint_auth_method=token_endpoint_auth_method,
|
token_endpoint_auth_method=token_endpoint_auth_method,
|
||||||
scope=scopes,
|
scope=scope,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _initialize(self) -> None:
|
async def _initialize(self) -> None:
|
||||||
|
|
@ -258,7 +258,7 @@ class PrivateKeyJWTOAuthProvider(OAuthClientProvider):
|
||||||
storage: TokenStorage,
|
storage: TokenStorage,
|
||||||
client_id: str,
|
client_id: str,
|
||||||
assertion_provider: Callable[[str], Awaitable[str]],
|
assertion_provider: Callable[[str], Awaitable[str]],
|
||||||
scopes: str | None = None,
|
scope: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize private_key_jwt OAuth provider.
|
"""Initialize private_key_jwt OAuth provider.
|
||||||
|
|
||||||
|
|
@ -271,14 +271,14 @@ class PrivateKeyJWTOAuthProvider(OAuthClientProvider):
|
||||||
`SignedJWTParameters.create_assertion_provider()` for SDK-signed JWTs,
|
`SignedJWTParameters.create_assertion_provider()` for SDK-signed JWTs,
|
||||||
`static_assertion_provider()` for pre-built JWTs, or provide your own
|
`static_assertion_provider()` for pre-built JWTs, or provide your own
|
||||||
callback for workload identity federation.
|
callback for workload identity federation.
|
||||||
scopes: Optional space-separated list of scopes to request.
|
scope: Optional space-separated list of scopes to request.
|
||||||
"""
|
"""
|
||||||
# Build minimal client_metadata for the base class
|
# Build minimal client_metadata for the base class
|
||||||
client_metadata = OAuthClientMetadata(
|
client_metadata = OAuthClientMetadata(
|
||||||
redirect_uris=None,
|
redirect_uris=None,
|
||||||
grant_types=["client_credentials"],
|
grant_types=["client_credentials"],
|
||||||
token_endpoint_auth_method="private_key_jwt",
|
token_endpoint_auth_method="private_key_jwt",
|
||||||
scope=scopes,
|
scope=scope,
|
||||||
)
|
)
|
||||||
super().__init__(server_url, client_metadata, storage, None, None, 300.0)
|
super().__init__(server_url, client_metadata, storage, None, None, 300.0)
|
||||||
self._assertion_provider = assertion_provider
|
self._assertion_provider = assertion_provider
|
||||||
|
|
@ -288,7 +288,7 @@ class PrivateKeyJWTOAuthProvider(OAuthClientProvider):
|
||||||
client_id=client_id,
|
client_id=client_id,
|
||||||
grant_types=["client_credentials"],
|
grant_types=["client_credentials"],
|
||||||
token_endpoint_auth_method="private_key_jwt",
|
token_endpoint_auth_method="private_key_jwt",
|
||||||
scope=scopes,
|
scope=scope,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _initialize(self) -> None:
|
async def _initialize(self) -> None:
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,7 @@ class TestClientCredentialsOAuthProvider:
|
||||||
storage=mock_storage,
|
storage=mock_storage,
|
||||||
client_id="test-client-id",
|
client_id="test-client-id",
|
||||||
client_secret="test-client-secret",
|
client_secret="test-client-secret",
|
||||||
scopes="read write",
|
scope="read write",
|
||||||
)
|
)
|
||||||
|
|
||||||
await provider._initialize()
|
await provider._initialize()
|
||||||
|
|
@ -240,7 +240,7 @@ class TestClientCredentialsOAuthProvider:
|
||||||
storage=mock_storage,
|
storage=mock_storage,
|
||||||
client_id="test-client-id",
|
client_id="test-client-id",
|
||||||
client_secret="test-client-secret",
|
client_secret="test-client-secret",
|
||||||
scopes="read write",
|
scope="read write",
|
||||||
)
|
)
|
||||||
provider.context.oauth_metadata = OAuthMetadata(
|
provider.context.oauth_metadata = OAuthMetadata(
|
||||||
issuer=AnyHttpUrl("https://api.example.com"),
|
issuer=AnyHttpUrl("https://api.example.com"),
|
||||||
|
|
@ -268,7 +268,7 @@ class TestClientCredentialsOAuthProvider:
|
||||||
client_id="test-client-id",
|
client_id="test-client-id",
|
||||||
client_secret="test-client-secret",
|
client_secret="test-client-secret",
|
||||||
token_endpoint_auth_method="client_secret_post",
|
token_endpoint_auth_method="client_secret_post",
|
||||||
scopes="read write",
|
scope="read write",
|
||||||
)
|
)
|
||||||
await provider._initialize()
|
await provider._initialize()
|
||||||
provider.context.oauth_metadata = OAuthMetadata(
|
provider.context.oauth_metadata = OAuthMetadata(
|
||||||
|
|
@ -296,7 +296,7 @@ class TestClientCredentialsOAuthProvider:
|
||||||
client_id="placeholder",
|
client_id="placeholder",
|
||||||
client_secret="test-client-secret",
|
client_secret="test-client-secret",
|
||||||
token_endpoint_auth_method="client_secret_post",
|
token_endpoint_auth_method="client_secret_post",
|
||||||
scopes="read write",
|
scope="read write",
|
||||||
)
|
)
|
||||||
await provider._initialize()
|
await provider._initialize()
|
||||||
provider.context.oauth_metadata = OAuthMetadata(
|
provider.context.oauth_metadata = OAuthMetadata(
|
||||||
|
|
@ -386,7 +386,7 @@ class TestPrivateKeyJWTOAuthProvider:
|
||||||
storage=mock_storage,
|
storage=mock_storage,
|
||||||
client_id="test-client-id",
|
client_id="test-client-id",
|
||||||
assertion_provider=mock_assertion_provider,
|
assertion_provider=mock_assertion_provider,
|
||||||
scopes="read write",
|
scope="read write",
|
||||||
)
|
)
|
||||||
provider.context.oauth_metadata = OAuthMetadata(
|
provider.context.oauth_metadata = OAuthMetadata(
|
||||||
issuer=AnyHttpUrl("https://auth.example.com"),
|
issuer=AnyHttpUrl("https://auth.example.com"),
|
||||||
|
|
|
||||||
|
|
@ -372,7 +372,7 @@ async def test_client_credentials_provider_obtains_a_token_without_an_authorize_
|
||||||
storage=InMemoryTokenStorage(),
|
storage=InMemoryTokenStorage(),
|
||||||
client_id="m2m-client",
|
client_id="m2m-client",
|
||||||
client_secret="m2m-secret",
|
client_secret="m2m-secret",
|
||||||
scopes="mcp",
|
scope="mcp",
|
||||||
)
|
)
|
||||||
|
|
||||||
with anyio.fail_after(5):
|
with anyio.fail_after(5):
|
||||||
|
|
@ -423,7 +423,7 @@ async def test_private_key_jwt_provider_authenticates_the_token_request_with_an_
|
||||||
storage=InMemoryTokenStorage(),
|
storage=InMemoryTokenStorage(),
|
||||||
client_id="m2m-jwt-client",
|
client_id="m2m-jwt-client",
|
||||||
assertion_provider=assertion_provider,
|
assertion_provider=assertion_provider,
|
||||||
scopes="mcp",
|
scope="mcp",
|
||||||
)
|
)
|
||||||
|
|
||||||
with anyio.fail_after(5):
|
with anyio.fail_after(5):
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue