Rename scopes= to scope= on the client-credentials OAuth providers (#3166)

This commit is contained in:
Max
2026-07-25 20:23:36 +01:00
committed by GitHub
parent 5dd062d077
commit e90a66b554
7 changed files with 34 additions and 18 deletions
+1 -1
View File
@@ -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.
+16
View File
@@ -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
+1 -1
View File
@@ -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"),
+2 -2
View File
@@ -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):