Skip to content

Commit e8d201f

Browse files
committed
Test the default principal binding across two subjects of one client
The unit test pinned the principal string; this drives the boundary end to end under two bearer identities sharing a client_id: state minted for one subject is rejected for the other and restores only for the minter.
1 parent eaeaa31 commit e8d201f

1 file changed

Lines changed: 62 additions & 0 deletions

File tree

tests/server/test_request_state_boundary.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
import mcp.server.request_state as request_state_module
2929
from mcp import Client
3030
from mcp.server import MCPServer, Server, ServerRequestContext
31+
from mcp.server.auth.middleware.auth_context import auth_context_var
32+
from mcp.server.auth.middleware.bearer_auth import AuthenticatedUser
33+
from mcp.server.auth.provider import AccessToken
3134
from mcp.server.context import HandlerResult
3235
from mcp.server.mcpserver import Context
3336
from mcp.server.request_state import (
@@ -1191,3 +1194,62 @@ async def test_a_fractional_mint_instant_keeps_the_full_ttl(monkeypatch: pytest.
11911194

11921195
assert isinstance(second, CallToolResult)
11931196
assert seen == ["awaiting-confirm"]
1197+
1198+
1199+
async def test_default_principal_distinguishes_two_subjects_of_one_oauth_client() -> None:
1200+
"""Spec-mandated (basic/patterns/mrtr server requirement 5, cross-user reuse): with the
1201+
default binding, state sealed for one user of an OAuth client is rejected for another
1202+
user of the same client and restored only for the original subject."""
1203+
boundary = RequestStateBoundary(RequestStateSecurity(keys=[_KEY]), default_audience="svc")
1204+
seen: list[str | None] = []
1205+
1206+
async def mint(ctx: ServerRequestContext[Any, Any]) -> HandlerResult:
1207+
return InputRequiredResult(input_requests={"confirm": _ask("PIN?")}, request_state="alice-secret")
1208+
1209+
async def restore(ctx: ServerRequestContext[Any, Any]) -> HandlerResult:
1210+
assert ctx.params is not None
1211+
seen.append(ctx.params["requestState"])
1212+
return CallToolResult(content=[TextContent(text="done")])
1213+
1214+
def request(token: str | None = None) -> ServerRequestContext[Any, Any]:
1215+
params: dict[str, Any] = {"name": "fetch_pin", "arguments": {}}
1216+
if token is not None:
1217+
params["requestState"] = token
1218+
return ServerRequestContext(
1219+
session=cast("Any", None),
1220+
lifespan_context={},
1221+
protocol_version="2026-07-28",
1222+
method="tools/call",
1223+
params=params,
1224+
)
1225+
1226+
def as_user(subject: str) -> AuthenticatedUser:
1227+
shared_client = "https://agent.example/client.json"
1228+
return AuthenticatedUser(
1229+
AccessToken(token=f"at-{subject}", client_id=shared_client, scopes=[], subject=subject)
1230+
)
1231+
1232+
reset = auth_context_var.set(as_user("alice"))
1233+
try:
1234+
sealed = await boundary(request(), mint)
1235+
finally:
1236+
auth_context_var.reset(reset)
1237+
assert isinstance(sealed, InputRequiredResult)
1238+
assert sealed.request_state is not None
1239+
1240+
reset = auth_context_var.set(as_user("bob"))
1241+
try:
1242+
with pytest.raises(MCPError) as exc:
1243+
await boundary(request(sealed.request_state), restore)
1244+
finally:
1245+
auth_context_var.reset(reset)
1246+
_assert_frozen_rejection(exc)
1247+
assert seen == []
1248+
1249+
reset = auth_context_var.set(as_user("alice"))
1250+
try:
1251+
result = await boundary(request(sealed.request_state), restore)
1252+
finally:
1253+
auth_context_var.reset(reset)
1254+
assert isinstance(result, CallToolResult)
1255+
assert seen == ["alice-secret"]

0 commit comments

Comments
 (0)