diff --git a/gns3server/api/routes/controller/dependencies/authentication.py b/gns3server/api/routes/controller/dependencies/authentication.py index 60c880894..1712155cd 100644 --- a/gns3server/api/routes/controller/dependencies/authentication.py +++ b/gns3server/api/routes/controller/dependencies/authentication.py @@ -15,6 +15,7 @@ # along with this program. If not, see . import asyncio +import hashlib import logging import bcrypt @@ -176,7 +177,14 @@ async def get_current_active_user_from_websocket( return user except HTTPException as e: - err_msg = f"Could not authenticate while connecting to controller WebSocket: {e.detail}" + # Fingerprint the received token so clients can compare it against the fingerprint + # returned when the token was issued (e.g. token_sha256_prefix from the + # node_console_info MCP tool) and detect copy corruption on their side. + token_sha256_prefix = hashlib.sha256(token.encode()).hexdigest()[:8] + err_msg = ( + f"Could not authenticate while connecting to controller WebSocket: {e.detail} " + f"(received token sha256 prefix: {token_sha256_prefix})" + ) websocket_error = {"action": "log.error", "event": {"message": err_msg}} await websocket.send_json(websocket_error) log.error(err_msg) diff --git a/gns3server/api/routes/mcp/nodes.py b/gns3server/api/routes/mcp/nodes.py index 0a7c1320c..3d3a7133a 100644 --- a/gns3server/api/routes/mcp/nodes.py +++ b/gns3server/api/routes/mcp/nodes.py @@ -25,6 +25,7 @@ via Gns3Connector (from custom_gns3fy). from typing import Any from concurrent.futures import ThreadPoolExecutor, as_completed +import hashlib import logging from gns3server.services import auth_service @@ -312,6 +313,12 @@ def get_node_console_info_handler(params: dict[str, Any], gns3_ctx: dict[str, An "ws_url": ws_url, "command": f"websocat -t --no-close {ws_url}", } + if ws_token: + # Fingerprint of the minted token: compare it against what actually reached the + # server (logged on WebSocket auth rejection) to detect copy corruption, and + # re-request the URL once token_ttl_seconds has elapsed. + result["token_sha256_prefix"] = hashlib.sha256(ws_token.encode()).hexdigest()[:8] + result["token_ttl_seconds"] = 600 if console_type in ("vnc",): result["vnc_url"] = f"/v3/projects/{project_id}/nodes/{node_id}/console/vnc?token={gns3_ctx['jwt_token']}" return result diff --git a/tests/api/routes/test_routes.py b/tests/api/routes/test_routes.py index 0854581f7..bc357d668 100644 --- a/tests/api/routes/test_routes.py +++ b/tests/api/routes/test_routes.py @@ -106,7 +106,8 @@ class TestRoutes: async with aconnect_ws(path, client, params=params) as ws: json_notification = await ws.receive_json() assert json_notification['event'] == { - 'message': 'Could not authenticate while connecting to controller WebSocket: Invalid token (DecodeError)' + 'message': 'Could not authenticate while connecting to controller WebSocket: ' + 'Invalid token (DecodeError) (received token sha256 prefix: 4d4f92fb)' }