Deprecate constructing the pre-provisioned OAuth clients without an issuer (#3435)
This commit is contained in:
parent
d060b36e1d
commit
979208c115
8 changed files with 103 additions and 11 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.
|
||||||
* `issuer` names the authorization server that issued those credentials; use the `issuer` value its `/.well-known/oauth-authorization-server` document returns. Discovery still runs as above, but token requests are only ever built from metadata for *that* issuer; if the MCP server points anywhere else, the flow stops with an `OAuthFlowError` instead. Leave it out and the provider uses whichever authorization server discovery finds.
|
* `issuer` names the authorization server that issued those credentials; use the `issuer` value its `/.well-known/oauth-authorization-server` document returns. Discovery still runs as above, but token requests are only ever built from metadata for *that* issuer; if the MCP server points anywhere else, the flow stops with an `OAuthFlowError` instead. Leaving it out is deprecated and it becomes required in 3.0 (see **[Deprecated features](../deprecated.md#deprecated-sdk-helpers)**); until then the provider warns and uses whichever authorization server discovery finds.
|
||||||
* `scope` 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`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Deprecated features
|
# Deprecated features
|
||||||
|
|
||||||
The 2026-07-28 spec retires five things. The SDK still implements every one of them, and every one of them now carries a **deprecation warning**. One SDK helper is deprecated on its own account and is listed [at the end](#deprecated-sdk-helpers).
|
The 2026-07-28 spec retires five things. The SDK still implements every one of them, and every one of them now carries a **deprecation warning**. A few SDK-level deprecations stand on their own account and are listed [at the end](#deprecated-sdk-helpers).
|
||||||
|
|
||||||
The table below names each deprecated feature, why it is going away, and the replacement to build on.
|
The table below names each deprecated feature, why it is going away, and the replacement to build on.
|
||||||
|
|
||||||
|
|
@ -131,11 +131,12 @@ That is the whole API. There is no per-method switch, and you don't want one: th
|
||||||
|
|
||||||
## Deprecated SDK helpers
|
## Deprecated SDK helpers
|
||||||
|
|
||||||
These are not spec changes, only SDK internals with a better replacement. They warn with the same `MCPDeprecationWarning` and will be removed in 3.0.
|
These are not spec changes, only SDK usage with a better replacement. They warn with the same `MCPDeprecationWarning`, and 3.0 removes the old form.
|
||||||
|
|
||||||
| Deprecated | What you do instead |
|
| Deprecated | What you do instead |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `FuncMetadata.call_fn_with_arg_validation()` | `FuncMetadata.validate_arguments()` and then `FuncMetadata.call_fn()`. Only code that drives `FuncMetadata` directly (a custom `Tool` subclass, say) ever called it. |
|
| `FuncMetadata.call_fn_with_arg_validation()` | `FuncMetadata.validate_arguments()` and then `FuncMetadata.call_fn()`. Only code that drives `FuncMetadata` directly (a custom `Tool` subclass, say) ever called it. |
|
||||||
|
| `ClientCredentialsOAuthProvider(...)` or `PrivateKeyJWTOAuthProvider(...)` without `issuer=` | Pass `issuer=` naming the authorization server that issued the credentials (see **[Writing OAuth clients](client/oauth-clients.md#machine-to-machine)**). Without it the MCP server decides which authorization server receives them; 3.0 makes the keyword required. |
|
||||||
|
|
||||||
## Recap
|
## Recap
|
||||||
|
|
||||||
|
|
@ -144,7 +145,7 @@ These are not spec changes, only SDK internals with a better replacement. They w
|
||||||
* Deprecated is advisory: no wire changes, everything keeps working against pre-2026 sessions, and you get a visible `MCPDeprecationWarning` (a `UserWarning`, so it is on by default).
|
* Deprecated is advisory: no wire changes, everything keeps working against pre-2026 sessions, and you get a visible `MCPDeprecationWarning` (a `UserWarning`, so it is on by default).
|
||||||
* Sampling and roots additionally need a back-channel that a 2026-07-28 session does not have. On a modern connection they warn and then they raise.
|
* Sampling and roots additionally need a back-channel that a 2026-07-28 session does not have. On a modern connection they warn and then they raise.
|
||||||
* `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` silences the whole category; `"error::mcp.MCPDeprecationWarning"` in pytest turns it into a test failure.
|
* `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` silences the whole category; `"error::mcp.MCPDeprecationWarning"` in pytest turns it into a test failure.
|
||||||
* One SDK helper, `FuncMetadata.call_fn_with_arg_validation()`, is deprecated separately for removal in 3.0.
|
* The [SDK-level deprecations](#deprecated-sdk-helpers) follow the same rule: they warn now, and 3.0 drops the old form.
|
||||||
* New code should not be built on any of these.
|
* New code should not be built on any of these.
|
||||||
|
|
||||||
Every other page in these docs teaches the current API.
|
Every other page in these docs teaches the current API.
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,8 @@ the client and server side.
|
||||||
- `client.py` `main` — opens with `async with Client(target, mode=mode) as
|
- `client.py` `main` — opens with `async with Client(target, mode=mode) as
|
||||||
client:` and that's the whole program. `target` is a transport that already
|
client:` and that's the whole program. `target` is a transport that already
|
||||||
carries the OAuth `httpx2.Auth`; the body never touches a token.
|
carries the OAuth `httpx2.Auth`; the body never touches a token.
|
||||||
- `client.py` `build_auth` — five lines of `ClientCredentialsOAuthProvider`
|
- `client.py` `build_auth` — a few lines of `ClientCredentialsOAuthProvider`
|
||||||
config is all the caller writes; the SDK does RFC 9728 PRM →
|
config (credentials plus `issuer=`) is all the caller writes; the SDK does RFC 9728 PRM →
|
||||||
RFC 8414 AS-metadata discovery and token exchange on the first 401.
|
RFC 8414 AS-metadata discovery and token exchange on the first 401.
|
||||||
- `server.py` `token_endpoint` — the *entire* AS for this grant: validate
|
- `server.py` `token_endpoint` — the *entire* AS for this grant: validate
|
||||||
HTTP-Basic `client_id:client_secret`, mint a token, return RFC 6749 JSON.
|
HTTP-Basic `client_id:client_secret`, mint a token, return RFC 6749 JSON.
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,13 @@ from stories._harness import Target, run_client
|
||||||
|
|
||||||
# MCP_URL pins the resource to :8000, and the server side builds its PRM/AS metadata from
|
# MCP_URL pins the resource to :8000, and the server side builds its PRM/AS metadata from
|
||||||
# the same constant — run the server on 8000 or the discovery chain points at the wrong origin.
|
# the same constant — run the server on 8000 or the discovery chain points at the wrong origin.
|
||||||
from stories._shared.auth import MCP_URL, InMemoryTokenStorage
|
from stories._shared.auth import BASE_URL, MCP_URL, InMemoryTokenStorage
|
||||||
|
|
||||||
from .server import DEMO_CLIENT_ID, DEMO_CLIENT_SECRET, DEMO_SCOPE
|
from .server import DEMO_CLIENT_ID, DEMO_CLIENT_SECRET, DEMO_SCOPE
|
||||||
|
|
||||||
|
|
||||||
def build_auth(_http: httpx2.AsyncClient) -> httpx2.Auth:
|
def build_auth(_http: httpx2.AsyncClient) -> httpx2.Auth:
|
||||||
"""The ``httpx2.Auth`` for the ``client_credentials`` grant — five lines of provider config.
|
"""The ``httpx2.Auth`` for the ``client_credentials`` grant — a few lines of provider config.
|
||||||
|
|
||||||
The SDK then handles 401 → RFC 9728 PRM → RFC 8414 AS-metadata discovery → token POST →
|
The SDK then handles 401 → RFC 9728 PRM → RFC 8414 AS-metadata discovery → token POST →
|
||||||
Bearer attachment automatically. ``Client(url)`` has no ``auth=`` passthrough yet, so the
|
Bearer attachment automatically. ``Client(url)`` has no ``auth=`` passthrough yet, so the
|
||||||
|
|
@ -27,6 +27,7 @@ def build_auth(_http: httpx2.AsyncClient) -> httpx2.Auth:
|
||||||
client_id=DEMO_CLIENT_ID,
|
client_id=DEMO_CLIENT_ID,
|
||||||
client_secret=DEMO_CLIENT_SECRET,
|
client_secret=DEMO_CLIENT_SECRET,
|
||||||
scope=DEMO_SCOPE,
|
scope=DEMO_SCOPE,
|
||||||
|
issuer=BASE_URL,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ Provides OAuth providers for machine-to-machine authentication flows:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
import warnings
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
@ -20,10 +21,20 @@ from mcp.client.auth import OAuthClientProvider, OAuthFlowError, TokenStorage
|
||||||
from mcp.client.auth.oauth2 import OAuthContext
|
from mcp.client.auth.oauth2 import OAuthContext
|
||||||
from mcp.client.auth.utils import issuers_match
|
from mcp.client.auth.utils import issuers_match
|
||||||
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata
|
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata
|
||||||
|
from mcp.shared.exceptions import MCPDeprecationWarning
|
||||||
|
|
||||||
|
|
||||||
def _checked_issuer(issuer: str | None) -> str | None:
|
def _checked_issuer(issuer: str | None) -> str | None:
|
||||||
if issuer is not None and urlparse(issuer).scheme not in ("http", "https"):
|
if issuer is None:
|
||||||
|
warnings.warn(
|
||||||
|
"Omitting `issuer` is deprecated and it will be required in 3.0. Without it, the MCP server "
|
||||||
|
"decides which authorization server receives this client's credentials; pass "
|
||||||
|
"issuer=<your authorization server's issuer URL> so they are only ever sent there.",
|
||||||
|
MCPDeprecationWarning,
|
||||||
|
stacklevel=3,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
if urlparse(issuer).scheme not in ("http", "https"):
|
||||||
raise ValueError(f"issuer must be the authorization server's http(s) issuer URL, got {issuer!r}")
|
raise ValueError(f"issuer must be the authorization server's http(s) issuer URL, got {issuer!r}")
|
||||||
return issuer
|
return issuer
|
||||||
|
|
||||||
|
|
@ -97,7 +108,8 @@ class ClientCredentialsOAuthProvider(OAuthClientProvider):
|
||||||
issuer: The issuer identifier of the authorization server that issued
|
issuer: The issuer identifier of the authorization server that issued
|
||||||
`client_id` and `client_secret`. When set, token requests are only built from
|
`client_id` and `client_secret`. When set, token requests are only built from
|
||||||
discovered authorization server metadata whose `issuer` is exactly this string;
|
discovered authorization server metadata whose `issuer` is exactly this string;
|
||||||
otherwise the flow stops with `OAuthFlowError`. When omitted, whichever
|
otherwise the flow stops with `OAuthFlowError`. Omitting it is deprecated
|
||||||
|
(`MCPDeprecationWarning`) and it will be required in 3.0; until then, whichever
|
||||||
authorization server discovery yields is used.
|
authorization server discovery yields is used.
|
||||||
"""
|
"""
|
||||||
# Build minimal client_metadata for the base class
|
# Build minimal client_metadata for the base class
|
||||||
|
|
@ -168,6 +180,7 @@ def static_assertion_provider(token: str) -> Callable[[str], Awaitable[str]]:
|
||||||
storage=my_token_storage,
|
storage=my_token_storage,
|
||||||
client_id="my-client-id",
|
client_id="my-client-id",
|
||||||
assertion_provider=static_assertion_provider(my_prebuilt_jwt),
|
assertion_provider=static_assertion_provider(my_prebuilt_jwt),
|
||||||
|
issuer="https://auth.example.com",
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -202,6 +215,7 @@ class SignedJWTParameters(BaseModel):
|
||||||
storage=my_token_storage,
|
storage=my_token_storage,
|
||||||
client_id="my-client-id",
|
client_id="my-client-id",
|
||||||
assertion_provider=jwt_params.create_assertion_provider(),
|
assertion_provider=jwt_params.create_assertion_provider(),
|
||||||
|
issuer="https://auth.example.com",
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
@ -267,6 +281,7 @@ class PrivateKeyJWTOAuthProvider(OAuthClientProvider):
|
||||||
storage=my_token_storage,
|
storage=my_token_storage,
|
||||||
client_id="my-client-id",
|
client_id="my-client-id",
|
||||||
assertion_provider=get_workload_identity_token,
|
assertion_provider=get_workload_identity_token,
|
||||||
|
issuer="https://auth.example.com",
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -280,6 +295,7 @@ class PrivateKeyJWTOAuthProvider(OAuthClientProvider):
|
||||||
storage=my_token_storage,
|
storage=my_token_storage,
|
||||||
client_id="my-client-id",
|
client_id="my-client-id",
|
||||||
assertion_provider=static_assertion_provider(my_prebuilt_jwt),
|
assertion_provider=static_assertion_provider(my_prebuilt_jwt),
|
||||||
|
issuer="https://auth.example.com",
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -298,6 +314,7 @@ class PrivateKeyJWTOAuthProvider(OAuthClientProvider):
|
||||||
storage=my_token_storage,
|
storage=my_token_storage,
|
||||||
client_id="my-client-id",
|
client_id="my-client-id",
|
||||||
assertion_provider=jwt_params.create_assertion_provider(),
|
assertion_provider=jwt_params.create_assertion_provider(),
|
||||||
|
issuer="https://auth.example.com",
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
@ -327,7 +344,8 @@ class PrivateKeyJWTOAuthProvider(OAuthClientProvider):
|
||||||
registered with. When set, an assertion is only minted, and token requests
|
registered with. When set, an assertion is only minted, and token requests
|
||||||
are only built, once authorization server metadata whose `issuer` is exactly this
|
are only built, once authorization server metadata whose `issuer` is exactly this
|
||||||
string has been discovered; otherwise the flow stops with `OAuthFlowError`.
|
string has been discovered; otherwise the flow stops with `OAuthFlowError`.
|
||||||
When omitted, whichever authorization server discovery yields is used.
|
Omitting it is deprecated (`MCPDeprecationWarning`) and it will be required in
|
||||||
|
3.0; until then, whichever authorization server discovery yields is used.
|
||||||
"""
|
"""
|
||||||
# Build minimal client_metadata for the base class
|
# Build minimal client_metadata for the base class
|
||||||
client_metadata = OAuthClientMetadata(
|
client_metadata = OAuthClientMetadata(
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import pytest
|
||||||
from inline_snapshot import snapshot
|
from inline_snapshot import snapshot
|
||||||
from pydantic import AnyHttpUrl
|
from pydantic import AnyHttpUrl
|
||||||
|
|
||||||
|
from mcp import MCPDeprecationWarning
|
||||||
from mcp.client.auth import OAuthClientProvider, OAuthFlowError
|
from mcp.client.auth import OAuthClientProvider, OAuthFlowError
|
||||||
from mcp.client.auth.extensions.client_credentials import (
|
from mcp.client.auth.extensions.client_credentials import (
|
||||||
ClientCredentialsOAuthProvider,
|
ClientCredentialsOAuthProvider,
|
||||||
|
|
@ -57,6 +58,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",
|
||||||
|
issuer="https://api.example.com",
|
||||||
)
|
)
|
||||||
|
|
||||||
# client_info is set during _initialize
|
# client_info is set during _initialize
|
||||||
|
|
@ -77,6 +79,7 @@ class TestClientCredentialsOAuthProvider:
|
||||||
client_id="test-client-id",
|
client_id="test-client-id",
|
||||||
client_secret="test-client-secret",
|
client_secret="test-client-secret",
|
||||||
scope="read write",
|
scope="read write",
|
||||||
|
issuer="https://api.example.com",
|
||||||
)
|
)
|
||||||
|
|
||||||
await provider._initialize()
|
await provider._initialize()
|
||||||
|
|
@ -92,6 +95,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",
|
||||||
|
issuer="https://api.example.com",
|
||||||
)
|
)
|
||||||
|
|
||||||
await provider._initialize()
|
await provider._initialize()
|
||||||
|
|
@ -107,6 +111,7 @@ class TestClientCredentialsOAuthProvider:
|
||||||
client_id="test-client-id",
|
client_id="test-client-id",
|
||||||
client_secret="test-client-secret",
|
client_secret="test-client-secret",
|
||||||
scope="read write",
|
scope="read write",
|
||||||
|
issuer="https://api.example.com",
|
||||||
)
|
)
|
||||||
provider.context.oauth_metadata = OAuthMetadata(
|
provider.context.oauth_metadata = OAuthMetadata(
|
||||||
issuer=AnyHttpUrl("https://api.example.com"),
|
issuer=AnyHttpUrl("https://api.example.com"),
|
||||||
|
|
@ -135,6 +140,7 @@ class TestClientCredentialsOAuthProvider:
|
||||||
client_secret="test-client-secret",
|
client_secret="test-client-secret",
|
||||||
token_endpoint_auth_method="client_secret_post",
|
token_endpoint_auth_method="client_secret_post",
|
||||||
scope="read write",
|
scope="read write",
|
||||||
|
issuer="https://api.example.com",
|
||||||
)
|
)
|
||||||
await provider._initialize()
|
await provider._initialize()
|
||||||
provider.context.oauth_metadata = OAuthMetadata(
|
provider.context.oauth_metadata = OAuthMetadata(
|
||||||
|
|
@ -161,6 +167,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",
|
||||||
|
issuer="https://api.example.com",
|
||||||
)
|
)
|
||||||
provider.context.oauth_metadata = OAuthMetadata(
|
provider.context.oauth_metadata = OAuthMetadata(
|
||||||
issuer=AnyHttpUrl("https://api.example.com"),
|
issuer=AnyHttpUrl("https://api.example.com"),
|
||||||
|
|
@ -192,6 +199,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,
|
||||||
|
issuer="https://api.example.com",
|
||||||
)
|
)
|
||||||
|
|
||||||
# client_info is set during _initialize
|
# client_info is set during _initialize
|
||||||
|
|
@ -215,6 +223,7 @@ class TestPrivateKeyJWTOAuthProvider:
|
||||||
client_id="test-client-id",
|
client_id="test-client-id",
|
||||||
assertion_provider=mock_assertion_provider,
|
assertion_provider=mock_assertion_provider,
|
||||||
scope="read write",
|
scope="read write",
|
||||||
|
issuer="https://auth.example.com",
|
||||||
)
|
)
|
||||||
provider.context.oauth_metadata = OAuthMetadata(
|
provider.context.oauth_metadata = OAuthMetadata(
|
||||||
issuer=AnyHttpUrl("https://auth.example.com"),
|
issuer=AnyHttpUrl("https://auth.example.com"),
|
||||||
|
|
@ -246,6 +255,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,
|
||||||
|
issuer="https://auth.example.com",
|
||||||
)
|
)
|
||||||
provider.context.oauth_metadata = OAuthMetadata(
|
provider.context.oauth_metadata = OAuthMetadata(
|
||||||
issuer=AnyHttpUrl("https://auth.example.com"),
|
issuer=AnyHttpUrl("https://auth.example.com"),
|
||||||
|
|
@ -436,6 +446,65 @@ async def test_provider_picks_its_configured_issuer_among_several_advertised_ser
|
||||||
await flow.aclose()
|
await flow.aclose()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("kind", ["secret", "jwt"])
|
||||||
|
def test_constructing_without_issuer_is_deprecated(mock_storage: MockTokenStorage, kind: str) -> None:
|
||||||
|
"""SDK-defined: leaving `issuer` out is allowed, and the provider says at construction that
|
||||||
|
token requests will follow whichever authorization server the MCP server advertises."""
|
||||||
|
|
||||||
|
async def assertion_provider(audience: str) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
with pytest.warns(MCPDeprecationWarning) as recorded:
|
||||||
|
if kind == "secret":
|
||||||
|
ClientCredentialsOAuthProvider(
|
||||||
|
server_url=_SERVER_URL, storage=mock_storage, client_id="c", client_secret="s"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
PrivateKeyJWTOAuthProvider(
|
||||||
|
server_url=_SERVER_URL, storage=mock_storage, client_id="c", assertion_provider=assertion_provider
|
||||||
|
)
|
||||||
|
|
||||||
|
[warning] = recorded
|
||||||
|
assert warning.filename == __file__
|
||||||
|
assert str(warning.message) == (
|
||||||
|
"Omitting `issuer` is deprecated and it will be required in 3.0. Without it, the MCP server "
|
||||||
|
"decides which authorization server receives this client's credentials; pass "
|
||||||
|
"issuer=<your authorization server's issuer URL> so they are only ever sent there."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
@pytest.mark.parametrize("kind", ["secret", "jwt"])
|
||||||
|
async def test_without_issuer_the_exchange_follows_whichever_server_was_discovered(
|
||||||
|
mock_storage: MockTokenStorage, kind: str
|
||||||
|
) -> None:
|
||||||
|
"""SDK-defined: with no `issuer` configured the token request is built from whatever metadata
|
||||||
|
discovery produced, as before."""
|
||||||
|
|
||||||
|
async def assertion_provider(audience: str) -> str:
|
||||||
|
return "jwt"
|
||||||
|
|
||||||
|
with pytest.warns(MCPDeprecationWarning, match="Omitting `issuer` is deprecated"):
|
||||||
|
if kind == "secret":
|
||||||
|
provider: OAuthClientProvider = ClientCredentialsOAuthProvider(
|
||||||
|
server_url=_SERVER_URL, storage=mock_storage, client_id="c", client_secret="s"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
provider = PrivateKeyJWTOAuthProvider(
|
||||||
|
server_url=_SERVER_URL, storage=mock_storage, client_id="c", assertion_provider=assertion_provider
|
||||||
|
)
|
||||||
|
flow = provider.async_auth_flow(httpx2.Request("POST", _SERVER_URL))
|
||||||
|
|
||||||
|
token_request = await _answer_discovery(
|
||||||
|
flow,
|
||||||
|
authorization_server="https://elsewhere.example.com",
|
||||||
|
metadata=_metadata_for("https://elsewhere.example.com"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (token_request.method, str(token_request.url)) == ("POST", "https://elsewhere.example.com/token")
|
||||||
|
await flow.aclose()
|
||||||
|
|
||||||
|
|
||||||
def test_an_issuer_that_is_not_an_http_url_is_rejected_at_construction(mock_storage: MockTokenStorage) -> None:
|
def test_an_issuer_that_is_not_an_http_url_is_rejected_at_construction(mock_storage: MockTokenStorage) -> None:
|
||||||
"""SDK-defined: `issuer=` is the authorization server's issuer URL; anything else is a configuration
|
"""SDK-defined: `issuer=` is the authorization server's issuer URL; anything else is a configuration
|
||||||
error on both machine-to-machine providers."""
|
error on both machine-to-machine providers."""
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,7 @@ async def test_the_one_more_provider_is_private_key_jwt() -> None:
|
||||||
storage=tutorial002.InMemoryTokenStorage(),
|
storage=tutorial002.InMemoryTokenStorage(),
|
||||||
client_id="reporting-agent",
|
client_id="reporting-agent",
|
||||||
assertion_provider=static_assertion_provider("a.prebuilt.jwt"),
|
assertion_provider=static_assertion_provider("a.prebuilt.jwt"),
|
||||||
|
issuer="http://localhost:9000",
|
||||||
)
|
)
|
||||||
assert isinstance(provider, OAuthClientProvider)
|
assert isinstance(provider, OAuthClientProvider)
|
||||||
assert isinstance(provider, httpx2.Auth)
|
assert isinstance(provider, httpx2.Auth)
|
||||||
|
|
|
||||||
|
|
@ -373,6 +373,7 @@ async def test_client_credentials_provider_obtains_a_token_without_an_authorize_
|
||||||
client_id="m2m-client",
|
client_id="m2m-client",
|
||||||
client_secret="m2m-secret",
|
client_secret="m2m-secret",
|
||||||
scope="mcp",
|
scope="mcp",
|
||||||
|
issuer=BASE_URL,
|
||||||
)
|
)
|
||||||
|
|
||||||
with anyio.fail_after(5):
|
with anyio.fail_after(5):
|
||||||
|
|
@ -424,6 +425,7 @@ async def test_private_key_jwt_provider_authenticates_the_token_request_with_an_
|
||||||
client_id="m2m-jwt-client",
|
client_id="m2m-jwt-client",
|
||||||
assertion_provider=assertion_provider,
|
assertion_provider=assertion_provider,
|
||||||
scope="mcp",
|
scope="mcp",
|
||||||
|
issuer=BASE_URL,
|
||||||
)
|
)
|
||||||
|
|
||||||
with anyio.fail_after(5):
|
with anyio.fail_after(5):
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue