Rename scopes= to scope= on the client-credentials OAuth providers (#3166)
This commit is contained in:
@@ -112,7 +112,7 @@ A nightly job, a CI step, another service. There is no browser and nobody to cli
|
||||
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.
|
||||
* `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`.
|
||||
|
||||
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.
|
||||
|
||||
### `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`
|
||||
|
||||
During OAuth discovery, `OAuthClientProvider` now validates that the authorization server
|
||||
|
||||
@@ -29,7 +29,7 @@ oauth = ClientCredentialsOAuthProvider(
|
||||
storage=InMemoryTokenStorage(),
|
||||
client_id="reporting-agent",
|
||||
client_secret="...",
|
||||
scopes="user",
|
||||
scope="user",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ def build_auth(_http: httpx2.AsyncClient) -> httpx2.Auth:
|
||||
storage=InMemoryTokenStorage(),
|
||||
client_id=DEMO_CLIENT_ID,
|
||||
client_secret=DEMO_CLIENT_SECRET,
|
||||
scopes=DEMO_SCOPE,
|
||||
scope=DEMO_SCOPE,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ class ClientCredentialsOAuthProvider(OAuthClientProvider):
|
||||
client_id: str,
|
||||
client_secret: str,
|
||||
token_endpoint_auth_method: Literal["client_secret_basic", "client_secret_post"] = "client_secret_basic",
|
||||
scopes: str | None = None,
|
||||
scope: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize client_credentials OAuth provider.
|
||||
|
||||
@@ -57,14 +57,14 @@ class ClientCredentialsOAuthProvider(OAuthClientProvider):
|
||||
client_secret: The OAuth client secret.
|
||||
token_endpoint_auth_method: Authentication method for token endpoint.
|
||||
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
|
||||
client_metadata = OAuthClientMetadata(
|
||||
redirect_uris=None,
|
||||
grant_types=["client_credentials"],
|
||||
token_endpoint_auth_method=token_endpoint_auth_method,
|
||||
scope=scopes,
|
||||
scope=scope,
|
||||
)
|
||||
super().__init__(server_url, client_metadata, storage, None, None, 300.0)
|
||||
# Store client_info to be set during _initialize - no dynamic registration needed
|
||||
@@ -74,7 +74,7 @@ class ClientCredentialsOAuthProvider(OAuthClientProvider):
|
||||
client_secret=client_secret,
|
||||
grant_types=["client_credentials"],
|
||||
token_endpoint_auth_method=token_endpoint_auth_method,
|
||||
scope=scopes,
|
||||
scope=scope,
|
||||
)
|
||||
|
||||
async def _initialize(self) -> None:
|
||||
@@ -258,7 +258,7 @@ class PrivateKeyJWTOAuthProvider(OAuthClientProvider):
|
||||
storage: TokenStorage,
|
||||
client_id: str,
|
||||
assertion_provider: Callable[[str], Awaitable[str]],
|
||||
scopes: str | None = None,
|
||||
scope: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize private_key_jwt OAuth provider.
|
||||
|
||||
@@ -271,14 +271,14 @@ class PrivateKeyJWTOAuthProvider(OAuthClientProvider):
|
||||
`SignedJWTParameters.create_assertion_provider()` for SDK-signed JWTs,
|
||||
`static_assertion_provider()` for pre-built JWTs, or provide your own
|
||||
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
|
||||
client_metadata = OAuthClientMetadata(
|
||||
redirect_uris=None,
|
||||
grant_types=["client_credentials"],
|
||||
token_endpoint_auth_method="private_key_jwt",
|
||||
scope=scopes,
|
||||
scope=scope,
|
||||
)
|
||||
super().__init__(server_url, client_metadata, storage, None, None, 300.0)
|
||||
self._assertion_provider = assertion_provider
|
||||
@@ -288,7 +288,7 @@ class PrivateKeyJWTOAuthProvider(OAuthClientProvider):
|
||||
client_id=client_id,
|
||||
grant_types=["client_credentials"],
|
||||
token_endpoint_auth_method="private_key_jwt",
|
||||
scope=scopes,
|
||||
scope=scope,
|
||||
)
|
||||
|
||||
async def _initialize(self) -> None:
|
||||
|
||||
@@ -210,7 +210,7 @@ class TestClientCredentialsOAuthProvider:
|
||||
storage=mock_storage,
|
||||
client_id="test-client-id",
|
||||
client_secret="test-client-secret",
|
||||
scopes="read write",
|
||||
scope="read write",
|
||||
)
|
||||
|
||||
await provider._initialize()
|
||||
@@ -240,7 +240,7 @@ class TestClientCredentialsOAuthProvider:
|
||||
storage=mock_storage,
|
||||
client_id="test-client-id",
|
||||
client_secret="test-client-secret",
|
||||
scopes="read write",
|
||||
scope="read write",
|
||||
)
|
||||
provider.context.oauth_metadata = OAuthMetadata(
|
||||
issuer=AnyHttpUrl("https://api.example.com"),
|
||||
@@ -268,7 +268,7 @@ class TestClientCredentialsOAuthProvider:
|
||||
client_id="test-client-id",
|
||||
client_secret="test-client-secret",
|
||||
token_endpoint_auth_method="client_secret_post",
|
||||
scopes="read write",
|
||||
scope="read write",
|
||||
)
|
||||
await provider._initialize()
|
||||
provider.context.oauth_metadata = OAuthMetadata(
|
||||
@@ -296,7 +296,7 @@ class TestClientCredentialsOAuthProvider:
|
||||
client_id="placeholder",
|
||||
client_secret="test-client-secret",
|
||||
token_endpoint_auth_method="client_secret_post",
|
||||
scopes="read write",
|
||||
scope="read write",
|
||||
)
|
||||
await provider._initialize()
|
||||
provider.context.oauth_metadata = OAuthMetadata(
|
||||
@@ -386,7 +386,7 @@ class TestPrivateKeyJWTOAuthProvider:
|
||||
storage=mock_storage,
|
||||
client_id="test-client-id",
|
||||
assertion_provider=mock_assertion_provider,
|
||||
scopes="read write",
|
||||
scope="read write",
|
||||
)
|
||||
provider.context.oauth_metadata = OAuthMetadata(
|
||||
issuer=AnyHttpUrl("https://auth.example.com"),
|
||||
|
||||
@@ -372,7 +372,7 @@ async def test_client_credentials_provider_obtains_a_token_without_an_authorize_
|
||||
storage=InMemoryTokenStorage(),
|
||||
client_id="m2m-client",
|
||||
client_secret="m2m-secret",
|
||||
scopes="mcp",
|
||||
scope="mcp",
|
||||
)
|
||||
|
||||
with anyio.fail_after(5):
|
||||
@@ -423,7 +423,7 @@ async def test_private_key_jwt_provider_authenticates_the_token_request_with_an_
|
||||
storage=InMemoryTokenStorage(),
|
||||
client_id="m2m-jwt-client",
|
||||
assertion_provider=assertion_provider,
|
||||
scopes="mcp",
|
||||
scope="mcp",
|
||||
)
|
||||
|
||||
with anyio.fail_after(5):
|
||||
|
||||
Reference in New Issue
Block a user