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:
Inna Harper 2025-06-23 14:19:03 +01:00 • committed by GitHub
commit 17f9c00c53
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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):