mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-21 16:18:55 +00:00
fix(mcp): serialize hosted oauth reauthorization
This commit is contained in:
parent
b09f1ba770
commit
11eaa77daf
4 changed files with 56 additions and 0 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue