MCP server separation into Authorization Server (AS) and Resource Server (RS) roles per spec PR #338 (#982)
Co-authored-by: Paul Carleton <paulc@anthropic.com>
This commit is contained in:
parent
d0443a1832
commit
17f9c00c53
1 changed files with 1851 additions and 1603 deletions
|
|
@ -8,7 +8,6 @@ from typing import Any, cast
|
|||
import pytest
|
||||
from starlette.authentication import AuthCredentials
|
||||
from starlette.datastructures import Headers
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.types import Message, Receive, Scope, Send
|
||||
|
||||
|
|
@ -20,6 +19,7 @@ from mcp.server.auth.middleware.bearer_auth import (
|
|||
from mcp.server.auth.provider import (
|
||||
AccessToken,
|
||||
OAuthAuthorizationServerProvider,
|
||||
ProviderTokenVerifier,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -118,14 +118,14 @@ class TestBearerAuthBackend:
|
|||
|
||||
async def test_no_auth_header(self, mock_oauth_provider: OAuthAuthorizationServerProvider[Any, Any, Any]):
|
||||
"""Test authentication with no Authorization header."""
|
||||
backend = BearerAuthBackend(provider=mock_oauth_provider)
|
||||
backend = BearerAuthBackend(token_verifier=ProviderTokenVerifier(mock_oauth_provider))
|
||||
request = Request({"type": "http", "headers": []})
|
||||
result = await backend.authenticate(request)
|
||||
assert result is None
|
||||
|
||||
async def test_non_bearer_auth_header(self, mock_oauth_provider: OAuthAuthorizationServerProvider[Any, Any, Any]):
|
||||
"""Test authentication with non-Bearer Authorization header."""
|
||||
backend = BearerAuthBackend(provider=mock_oauth_provider)
|
||||
backend = BearerAuthBackend(token_verifier=ProviderTokenVerifier(mock_oauth_provider))
|
||||
request = Request(
|
||||
{
|
||||
"type": "http",
|
||||
|
|
@ -137,7 +137,7 @@ class TestBearerAuthBackend:
|
|||
|
||||
async def test_invalid_token(self, mock_oauth_provider: OAuthAuthorizationServerProvider[Any, Any, Any]):
|
||||
"""Test authentication with invalid token."""
|
||||
backend = BearerAuthBackend(provider=mock_oauth_provider)
|
||||
backend = BearerAuthBackend(token_verifier=ProviderTokenVerifier(mock_oauth_provider))
|
||||
request = Request(
|
||||
{
|
||||
"type": "http",
|
||||
|
|
@ -153,7 +153,7 @@ class TestBearerAuthBackend:
|
|||
expired_access_token: AccessToken,
|
||||
):
|
||||
"""Test authentication with expired token."""
|
||||
backend = BearerAuthBackend(provider=mock_oauth_provider)
|
||||
backend = BearerAuthBackend(token_verifier=ProviderTokenVerifier(mock_oauth_provider))
|
||||
add_token_to_provider(mock_oauth_provider, "expired_token", expired_access_token)
|
||||
request = Request(
|
||||
{
|
||||
|
|
@ -170,7 +170,7 @@ class TestBearerAuthBackend:
|
|||
valid_access_token: AccessToken,
|
||||
):
|
||||
"""Test authentication with valid token."""
|
||||
backend = BearerAuthBackend(provider=mock_oauth_provider)
|
||||
backend = BearerAuthBackend(token_verifier=ProviderTokenVerifier(mock_oauth_provider))
|
||||
add_token_to_provider(mock_oauth_provider, "valid_token", valid_access_token)
|
||||
request = Request(
|
||||
{
|
||||
|
|
@ -194,7 +194,7 @@ class TestBearerAuthBackend:
|
|||
no_expiry_access_token: AccessToken,
|
||||
):
|
||||
"""Test authentication with token that has no expiry."""
|
||||
backend = BearerAuthBackend(provider=mock_oauth_provider)
|
||||
backend = BearerAuthBackend(token_verifier=ProviderTokenVerifier(mock_oauth_provider))
|
||||
add_token_to_provider(mock_oauth_provider, "no_expiry_token", no_expiry_access_token)
|
||||
request = Request(
|
||||
{
|
||||
|
|
@ -218,7 +218,7 @@ class TestBearerAuthBackend:
|
|||
valid_access_token: AccessToken,
|
||||
):
|
||||
"""Test with lowercase 'bearer' prefix in Authorization header"""
|
||||
backend = BearerAuthBackend(provider=mock_oauth_provider)
|
||||
backend = BearerAuthBackend(token_verifier=ProviderTokenVerifier(mock_oauth_provider))
|
||||
add_token_to_provider(mock_oauth_provider, "valid_token", valid_access_token)
|
||||
headers = Headers({"Authorization": "bearer valid_token"})
|
||||
scope = {"type": "http", "headers": headers.raw}
|
||||
|
|
@ -238,7 +238,7 @@ class TestBearerAuthBackend:
|
|||
valid_access_token: AccessToken,
|
||||
):
|
||||
"""Test with mixed 'BeArEr' prefix in Authorization header"""
|
||||
backend = BearerAuthBackend(provider=mock_oauth_provider)
|
||||
backend = BearerAuthBackend(token_verifier=ProviderTokenVerifier(mock_oauth_provider))
|
||||
add_token_to_provider(mock_oauth_provider, "valid_token", valid_access_token)
|
||||
headers = Headers({"authorization": "BeArEr valid_token"})
|
||||
scope = {"type": "http", "headers": headers.raw}
|
||||
|
|
@ -258,7 +258,7 @@ class TestBearerAuthBackend:
|
|||
valid_access_token: AccessToken,
|
||||
):
|
||||
"""Test authentication with mixed 'Authorization' header."""
|
||||
backend = BearerAuthBackend(provider=mock_oauth_provider)
|
||||
backend = BearerAuthBackend(token_verifier=ProviderTokenVerifier(mock_oauth_provider))
|
||||
add_token_to_provider(mock_oauth_provider, "valid_token", valid_access_token)
|
||||
headers = Headers({"AuThOrIzAtIoN": "BeArEr valid_token"})
|
||||
scope = {"type": "http", "headers": headers.raw}
|
||||
|
|
@ -287,14 +287,18 @@ class TestRequireAuthMiddleware:
|
|||
async def receive() -> Message:
|
||||
return {"type": "http.request"}
|
||||
|
||||
sent_messages = []
|
||||
|
||||
async def send(message: Message) -> None:
|
||||
pass
|
||||
sent_messages.append(message)
|
||||
|
||||
with pytest.raises(HTTPException) as excinfo:
|
||||
await middleware(scope, receive, send)
|
||||
await middleware(scope, receive, send)
|
||||
|
||||
assert excinfo.value.status_code == 401
|
||||
assert excinfo.value.detail == "Unauthorized"
|
||||
# Check that a 401 response was sent
|
||||
assert len(sent_messages) == 2
|
||||
assert sent_messages[0]["type"] == "http.response.start"
|
||||
assert sent_messages[0]["status"] == 401
|
||||
assert any(h[0] == b"www-authenticate" for h in sent_messages[0]["headers"])
|
||||
assert not app.called
|
||||
|
||||
async def test_non_authenticated_user(self):
|
||||
|
|
@ -307,14 +311,18 @@ class TestRequireAuthMiddleware:
|
|||
async def receive() -> Message:
|
||||
return {"type": "http.request"}
|
||||
|
||||
sent_messages = []
|
||||
|
||||
async def send(message: Message) -> None:
|
||||
pass
|
||||
sent_messages.append(message)
|
||||
|
||||
with pytest.raises(HTTPException) as excinfo:
|
||||
await middleware(scope, receive, send)
|
||||
await middleware(scope, receive, send)
|
||||
|
||||
assert excinfo.value.status_code == 401
|
||||
assert excinfo.value.detail == "Unauthorized"
|
||||
# Check that a 401 response was sent
|
||||
assert len(sent_messages) == 2
|
||||
assert sent_messages[0]["type"] == "http.response.start"
|
||||
assert sent_messages[0]["status"] == 401
|
||||
assert any(h[0] == b"www-authenticate" for h in sent_messages[0]["headers"])
|
||||
assert not app.called
|
||||
|
||||
async def test_missing_required_scope(self, valid_access_token: AccessToken):
|
||||
|
|
@ -332,14 +340,18 @@ class TestRequireAuthMiddleware:
|
|||
async def receive() -> Message:
|
||||
return {"type": "http.request"}
|
||||
|
||||
sent_messages = []
|
||||
|
||||
async def send(message: Message) -> None:
|
||||
pass
|
||||
sent_messages.append(message)
|
||||
|
||||
with pytest.raises(HTTPException) as excinfo:
|
||||
await middleware(scope, receive, send)
|
||||
await middleware(scope, receive, send)
|
||||
|
||||
assert excinfo.value.status_code == 403
|
||||
assert excinfo.value.detail == "Insufficient scope"
|
||||
# Check that a 403 response was sent
|
||||
assert len(sent_messages) == 2
|
||||
assert sent_messages[0]["type"] == "http.response.start"
|
||||
assert sent_messages[0]["status"] == 403
|
||||
assert any(h[0] == b"www-authenticate" for h in sent_messages[0]["headers"])
|
||||
assert not app.called
|
||||
|
||||
async def test_no_auth_credentials(self, valid_access_token: AccessToken):
|
||||
|
|
@ -356,14 +368,18 @@ class TestRequireAuthMiddleware:
|
|||
async def receive() -> Message:
|
||||
return {"type": "http.request"}
|
||||
|
||||
sent_messages = []
|
||||
|
||||
async def send(message: Message) -> None:
|
||||
pass
|
||||
sent_messages.append(message)
|
||||
|
||||
with pytest.raises(HTTPException) as excinfo:
|
||||
await middleware(scope, receive, send)
|
||||
await middleware(scope, receive, send)
|
||||
|
||||
assert excinfo.value.status_code == 403
|
||||
assert excinfo.value.detail == "Insufficient scope"
|
||||
# Check that a 403 response was sent
|
||||
assert len(sent_messages) == 2
|
||||
assert sent_messages[0]["type"] == "http.response.start"
|
||||
assert sent_messages[0]["status"] == 403
|
||||
assert any(h[0] == b"www-authenticate" for h in sent_messages[0]["headers"])
|
||||
assert not app.called
|
||||
|
||||
async def test_has_required_scopes(self, valid_access_token: AccessToken):
|
||||
|
|
|
|||
Loading…
Reference in a new issue