Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/client/oauth-clients.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 16 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1954,6 +1954,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
Expand Down
2 changes: 1 addition & 1 deletion docs_src/oauth_clients/tutorial002.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ async def set_client_info(self, client_info: OAuthClientInformationFull) -> None
storage=InMemoryTokenStorage(),
client_id="reporting-agent",
client_secret="...",
scopes="user",
scope="user",
)


Expand Down
2 changes: 1 addition & 1 deletion examples/stories/oauth_client_credentials/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)


Expand Down
16 changes: 8 additions & 8 deletions src/mcp/client/auth/extensions/client_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def __init__(
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.

Expand All @@ -57,14 +57,14 @@ def __init__(
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
Expand All @@ -74,7 +74,7 @@ def __init__(
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:
Expand Down Expand Up @@ -258,7 +258,7 @@ def __init__(
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.

Expand All @@ -271,14 +271,14 @@ def __init__(
`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
Expand All @@ -288,7 +288,7 @@ def __init__(
client_id=client_id,
grant_types=["client_credentials"],
token_endpoint_auth_method="private_key_jwt",
scope=scopes,
scope=scope,
)

async def _initialize(self) -> None:
Expand Down
10 changes: 5 additions & 5 deletions tests/client/auth/extensions/test_client_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ async def test_init_with_scopes(self, mock_storage: MockTokenStorage):
storage=mock_storage,
client_id="test-client-id",
client_secret="test-client-secret",
scopes="read write",
scope="read write",
)

await provider._initialize()
Expand Down Expand Up @@ -240,7 +240,7 @@ async def test_exchange_token_client_credentials(self, mock_storage: MockTokenSt
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"),
Expand Down Expand Up @@ -268,7 +268,7 @@ async def test_exchange_token_client_secret_post_includes_client_id(self, mock_s
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(
Expand Down Expand Up @@ -296,7 +296,7 @@ async def test_exchange_token_client_secret_post_without_client_id(self, mock_st
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(
Expand Down Expand Up @@ -386,7 +386,7 @@ async def mock_assertion_provider(audience: str) -> str:
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"),
Expand Down
4 changes: 2 additions & 2 deletions tests/interaction/auth/test_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -423,7 +423,7 @@ async def assertion_provider(audience: str) -> str:
storage=InMemoryTokenStorage(),
client_id="m2m-jwt-client",
assertion_provider=assertion_provider,
scopes="mcp",
scope="mcp",
)

with anyio.fail_after(5):
Expand Down
Loading