fix(mcp): serialize hosted oauth reauthorization

This commit is contained in:
Ben Barclay 2026-07-17 14:00:09 +10:00 committed by Teknium
parent b09f1ba770
commit 11eaa77daf
4 changed files with 56 additions and 0 deletions

View file

@ -11439,6 +11439,15 @@ async def auth_mcp_server(name: str, request: Request, profile: Optional[str] =
status_code=429,
detail="Too many MCP OAuth flows are already in progress",
)
if any(
flow.server_name == name
and flow.status in {"starting", "authorization_required"}
for flow in _mcp_oauth_flows.values()
):
raise HTTPException(
status_code=409,
detail=f"MCP OAuth for '{name}' is already in progress",
)
with _profile_scope(profile):
servers = _get_mcp_servers()
if name not in servers:

View file

@ -159,6 +159,29 @@ def test_hosted_auth_start_bounds_pending_flow_registry():
assert response.status_code == 429
def test_hosted_auth_rejects_overlapping_flow_for_same_server():
from hermes_cli import web_server
from tools.mcp_dashboard_oauth import DashboardOAuthFlow
client = _client()
client.post(
"/api/mcp/servers",
json={"name": "reports", "url": "https://mcp.example/mcp", "auth": "oauth"},
)
existing = DashboardOAuthFlow(
flow_id="existing-reports",
server_name="reports",
profile="other-profile",
redirect_uri="https://agent.example/callback/existing",
)
web_server._mcp_oauth_flows[existing.flow_id] = existing
response = client.post("/api/mcp/servers/reports/auth")
assert response.status_code == 409
assert "already in progress" in response.text
def test_flow_status_does_not_expose_authorization_code():
from hermes_cli import web_server
from tools.mcp_dashboard_oauth import DashboardOAuthFlow

View file

@ -72,6 +72,28 @@ def test_dashboard_flow_rejects_second_callback():
flow.deliver_callback(code="second", state="state", error=None)
def test_dashboard_flow_cannot_resurrect_after_terminal_error():
from tools.mcp_dashboard_oauth import DashboardOAuthFlow
flow = DashboardOAuthFlow(
flow_id="flow-terminal",
server_name="reports",
profile=None,
redirect_uri="https://agent.example/mcp/oauth/callback/flow-terminal",
)
flow.mark_error("start timed out")
with pytest.raises(RuntimeError, match="already ended"):
asyncio.run(
flow.publish_authorization_url(
"https://idp.example/authorize?state=too-late"
)
)
assert flow.status == "error"
assert flow.authorization_url is None
def test_dashboard_context_overrides_redirect_and_handlers():
from tools.mcp_dashboard_oauth import (
DashboardOAuthFlow,

View file

@ -36,6 +36,8 @@ class DashboardOAuthFlow:
_callback_ready: threading.Event = field(default_factory=threading.Event, init=False, repr=False)
async def publish_authorization_url(self, url: str) -> None:
if self.status in {"approved", "error"}:
raise RuntimeError("OAuth flow already ended")
state = parse_qs(urlparse(url).query).get("state", [None])[0]
if not state:
raise ValueError("OAuth authorization URL did not include state")