d95f3a6922
The check in validate_scope rejected any requested scope not in the client's registered metadata. This broke the MCP spec's step-up authorization flow: when a server returns 403 insufficient_scope with a WWW-Authenticate challenge containing expanded scopes, the client (see client/auth/oauth2.py) re-authorizes with those scopes and the server would reject them. RFC 7591 Section 2 defines the scope field as scopes the client "can use", with no language restricting requests to that set. Scope policy enforcement belongs in OAuthAuthorizationServerProvider.authorize(), which can already raise AuthorizeError(error="invalid_scope", ...). The TypeScript SDK removed this check in #983 for the same reason. InvalidScopeError is removed as it was only raised from this path. Reported-by: nik1097 Github-Issue: #2216
71 lines
2.8 KiB
Python
71 lines
2.8 KiB
Python
"""Tests for OAuth 2.0 shared code."""
|
|
|
|
from mcp.shared.auth import OAuthClientMetadata, OAuthMetadata
|
|
|
|
|
|
def test_oauth():
|
|
"""Should not throw when parsing OAuth metadata."""
|
|
OAuthMetadata.model_validate(
|
|
{
|
|
"issuer": "https://example.com",
|
|
"authorization_endpoint": "https://example.com/oauth2/authorize",
|
|
"token_endpoint": "https://example.com/oauth2/token",
|
|
"scopes_supported": ["read", "write"],
|
|
"response_types_supported": ["code", "token"],
|
|
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"],
|
|
}
|
|
)
|
|
|
|
|
|
def test_oidc():
|
|
"""Should not throw when parsing OIDC metadata."""
|
|
OAuthMetadata.model_validate(
|
|
{
|
|
"issuer": "https://example.com",
|
|
"authorization_endpoint": "https://example.com/oauth2/authorize",
|
|
"token_endpoint": "https://example.com/oauth2/token",
|
|
"end_session_endpoint": "https://example.com/logout",
|
|
"id_token_signing_alg_values_supported": ["RS256"],
|
|
"jwks_uri": "https://example.com/.well-known/jwks.json",
|
|
"response_types_supported": ["code", "token"],
|
|
"revocation_endpoint": "https://example.com/oauth2/revoke",
|
|
"scopes_supported": ["openid", "read", "write"],
|
|
"subject_types_supported": ["public"],
|
|
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"],
|
|
"userinfo_endpoint": "https://example.com/oauth2/userInfo",
|
|
}
|
|
)
|
|
|
|
|
|
def test_oauth_with_jarm():
|
|
"""Should not throw when parsing OAuth metadata that includes JARM response modes."""
|
|
OAuthMetadata.model_validate(
|
|
{
|
|
"issuer": "https://example.com",
|
|
"authorization_endpoint": "https://example.com/oauth2/authorize",
|
|
"token_endpoint": "https://example.com/oauth2/token",
|
|
"scopes_supported": ["read", "write"],
|
|
"response_types_supported": ["code", "token"],
|
|
"response_modes_supported": [
|
|
"query",
|
|
"fragment",
|
|
"form_post",
|
|
"query.jwt",
|
|
"fragment.jwt",
|
|
"form_post.jwt",
|
|
"jwt",
|
|
],
|
|
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"],
|
|
}
|
|
)
|
|
|
|
|
|
def test_validate_scope_none_returns_none():
|
|
client = OAuthClientMetadata.model_validate({"redirect_uris": ["https://example.com/cb"]})
|
|
assert client.validate_scope(None) is None
|
|
|
|
|
|
def test_validate_scope_splits_requested():
|
|
client = OAuthClientMetadata.model_validate({"redirect_uris": ["https://example.com/cb"]})
|
|
assert client.validate_scope("read write admin") == ["read", "write", "admin"]
|