Remove the deprecated RFC7523OAuthClientProvider (#3169)
This commit is contained in:
parent
1963af52cc
commit
7163d8263f
2 changed files with 38 additions and 319 deletions
|
|
@ -1,26 +1,20 @@
|
|||
import urllib.parse
|
||||
import warnings
|
||||
|
||||
import jwt
|
||||
import pytest
|
||||
from pydantic import AnyHttpUrl, AnyUrl
|
||||
from pydantic import AnyHttpUrl
|
||||
|
||||
from mcp.client.auth.extensions.client_credentials import (
|
||||
ClientCredentialsOAuthProvider,
|
||||
JWTParameters,
|
||||
PrivateKeyJWTOAuthProvider,
|
||||
RFC7523OAuthClientProvider,
|
||||
SignedJWTParameters,
|
||||
static_assertion_provider,
|
||||
)
|
||||
from mcp.shared.auth import (
|
||||
AuthorizationCodeResult,
|
||||
OAuthClientInformationFull,
|
||||
OAuthClientMetadata,
|
||||
OAuthMetadata,
|
||||
OAuthToken,
|
||||
)
|
||||
from mcp.shared.exceptions import MCPDeprecationWarning
|
||||
|
||||
|
||||
class MockTokenStorage:
|
||||
|
|
@ -48,138 +42,6 @@ def mock_storage():
|
|||
return MockTokenStorage()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client_metadata():
|
||||
return OAuthClientMetadata(
|
||||
client_name="Test Client",
|
||||
client_uri=AnyHttpUrl("https://example.com"),
|
||||
redirect_uris=[AnyUrl("http://localhost:3030/callback")],
|
||||
scope="read write",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rfc7523_oauth_provider(client_metadata: OAuthClientMetadata, mock_storage: MockTokenStorage):
|
||||
async def redirect_handler(url: str) -> None: # pragma: no cover
|
||||
"""Mock redirect handler."""
|
||||
pass
|
||||
|
||||
async def callback_handler() -> AuthorizationCodeResult: # pragma: no cover
|
||||
"""Mock callback handler."""
|
||||
return AuthorizationCodeResult(code="test_auth_code", state="test_state")
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", MCPDeprecationWarning)
|
||||
return RFC7523OAuthClientProvider(
|
||||
server_url="https://api.example.com/v1/mcp",
|
||||
client_metadata=client_metadata,
|
||||
storage=mock_storage,
|
||||
redirect_handler=redirect_handler,
|
||||
callback_handler=callback_handler,
|
||||
)
|
||||
|
||||
|
||||
class TestOAuthFlowClientCredentials:
|
||||
"""Test OAuth flow behavior for client credentials flows."""
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_token_exchange_request_jwt_predefined(self, rfc7523_oauth_provider: RFC7523OAuthClientProvider):
|
||||
"""Test token exchange request building with a predefined JWT assertion."""
|
||||
# Set up required context
|
||||
rfc7523_oauth_provider.context.client_info = OAuthClientInformationFull(
|
||||
grant_types=["urn:ietf:params:oauth:grant-type:jwt-bearer"],
|
||||
token_endpoint_auth_method="private_key_jwt",
|
||||
redirect_uris=None,
|
||||
scope="read write",
|
||||
)
|
||||
rfc7523_oauth_provider.context.oauth_metadata = OAuthMetadata(
|
||||
issuer=AnyHttpUrl("https://api.example.com"),
|
||||
authorization_endpoint=AnyHttpUrl("https://api.example.com/authorize"),
|
||||
token_endpoint=AnyHttpUrl("https://api.example.com/token"),
|
||||
registration_endpoint=AnyHttpUrl("https://api.example.com/register"),
|
||||
)
|
||||
rfc7523_oauth_provider.context.client_metadata = rfc7523_oauth_provider.context.client_info
|
||||
rfc7523_oauth_provider.context.protocol_version = "2025-06-18"
|
||||
rfc7523_oauth_provider.jwt_parameters = JWTParameters(
|
||||
# https://www.jwt.io
|
||||
assertion="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30"
|
||||
)
|
||||
|
||||
request = await rfc7523_oauth_provider._exchange_token_jwt_bearer()
|
||||
|
||||
assert request.method == "POST"
|
||||
assert str(request.url) == "https://api.example.com/token"
|
||||
assert request.headers["Content-Type"] == "application/x-www-form-urlencoded"
|
||||
|
||||
# Check form data
|
||||
content = urllib.parse.unquote_plus(request.content.decode())
|
||||
assert "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" in content
|
||||
assert "scope=read write" in content
|
||||
assert "resource=https://api.example.com/v1/mcp" in content
|
||||
assert (
|
||||
"assertion=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30"
|
||||
in content
|
||||
)
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_token_exchange_request_jwt(self, rfc7523_oauth_provider: RFC7523OAuthClientProvider):
|
||||
"""Test token exchange request building wiith a generated JWT assertion."""
|
||||
# Set up required context
|
||||
rfc7523_oauth_provider.context.client_info = OAuthClientInformationFull(
|
||||
grant_types=["urn:ietf:params:oauth:grant-type:jwt-bearer"],
|
||||
token_endpoint_auth_method="private_key_jwt",
|
||||
redirect_uris=None,
|
||||
scope="read write",
|
||||
)
|
||||
rfc7523_oauth_provider.context.oauth_metadata = OAuthMetadata(
|
||||
issuer=AnyHttpUrl("https://api.example.com"),
|
||||
authorization_endpoint=AnyHttpUrl("https://api.example.com/authorize"),
|
||||
token_endpoint=AnyHttpUrl("https://api.example.com/token"),
|
||||
registration_endpoint=AnyHttpUrl("https://api.example.com/register"),
|
||||
)
|
||||
rfc7523_oauth_provider.context.client_metadata = rfc7523_oauth_provider.context.client_info
|
||||
rfc7523_oauth_provider.context.protocol_version = "2025-06-18"
|
||||
rfc7523_oauth_provider.jwt_parameters = JWTParameters(
|
||||
issuer="foo",
|
||||
subject="1234567890",
|
||||
claims={
|
||||
"name": "John Doe",
|
||||
"admin": True,
|
||||
"iat": 1516239022,
|
||||
},
|
||||
jwt_signing_algorithm="HS256",
|
||||
jwt_signing_key="a-string-secret-at-least-256-bits-long",
|
||||
jwt_lifetime_seconds=300,
|
||||
)
|
||||
|
||||
request = await rfc7523_oauth_provider._exchange_token_jwt_bearer()
|
||||
|
||||
assert request.method == "POST"
|
||||
assert str(request.url) == "https://api.example.com/token"
|
||||
assert request.headers["Content-Type"] == "application/x-www-form-urlencoded"
|
||||
|
||||
# Check form data
|
||||
content = urllib.parse.unquote_plus(request.content.decode()).split("&")
|
||||
assert "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" in content
|
||||
assert "scope=read write" in content
|
||||
assert "resource=https://api.example.com/v1/mcp" in content
|
||||
|
||||
# Check assertion
|
||||
assertion = next(param for param in content if param.startswith("assertion="))[len("assertion=") :]
|
||||
claims = jwt.decode(
|
||||
assertion,
|
||||
key="a-string-secret-at-least-256-bits-long",
|
||||
algorithms=["HS256"],
|
||||
audience="https://api.example.com/",
|
||||
subject="1234567890",
|
||||
issuer="foo",
|
||||
verify=True,
|
||||
)
|
||||
assert claims["name"] == "John Doe"
|
||||
assert claims["admin"]
|
||||
assert claims["iat"] == 1516239022
|
||||
|
||||
|
||||
class TestClientCredentialsOAuthProvider:
|
||||
"""Test ClientCredentialsOAuthProvider."""
|
||||
|
||||
|
|
|
|||
|
|
@ -10,11 +10,9 @@ from docs_src.oauth_clients import tutorial001, tutorial002
|
|||
from mcp.client.auth import OAuthClientProvider, OAuthFlowError, OAuthRegistrationError, OAuthTokenError, TokenStorage
|
||||
from mcp.client.auth.extensions.client_credentials import (
|
||||
PrivateKeyJWTOAuthProvider,
|
||||
RFC7523OAuthClientProvider,
|
||||
static_assertion_provider,
|
||||
)
|
||||
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
|
||||
from mcp.shared.exceptions import MCPDeprecationWarning
|
||||
|
||||
# See test_index.py for why this is a per-module mark and not a conftest hook.
|
||||
pytestmark = [pytest.mark.anyio, pytest.mark.filterwarnings("error::mcp.MCPDeprecationWarning")]
|
||||
|
|
@ -104,16 +102,6 @@ async def test_the_one_more_provider_is_private_key_jwt() -> None:
|
|||
assert provider.context.client_metadata.token_endpoint_auth_method == "private_key_jwt"
|
||||
|
||||
|
||||
async def test_the_page_does_not_count_the_deprecated_provider() -> None:
|
||||
"""Why the `!!! info` says *one* more provider: `RFC7523OAuthClientProvider` warns on construction."""
|
||||
with pytest.warns(MCPDeprecationWarning, match="RFC7523OAuthClientProvider is deprecated"):
|
||||
RFC7523OAuthClientProvider(
|
||||
server_url="http://localhost:8001/mcp",
|
||||
client_metadata=tutorial001.oauth.context.client_metadata,
|
||||
storage=tutorial001.InMemoryTokenStorage(),
|
||||
)
|
||||
|
||||
|
||||
async def test_every_oauth_error_is_an_oauth_flow_error() -> None:
|
||||
"""Catch `OAuthFlowError` and you have caught registration and token failures too."""
|
||||
assert issubclass(OAuthRegistrationError, OAuthFlowError)
|
||||
|
|
|
|||
Loading…
Reference in a new issue