fix(mcp): thread token_version into console/download token minting

Token revocation is a strict version check (token_data.token_version !=
user.token_version). create_access_token defaults token_version to 0, so the
short-lived JWTs minted for the console WebSocket URL (nodes) and the download
URLs (symbols, links) carried ver=0. Any user who had logged out at least once
(token_version >= 1, e.g. the default admin) got tokens rejected as "revoked"
on first use.

e433991cf fixed this in _resolve_token's API-key branch but missed these three
independent minting sites. Now resolve token_version during _resolve_token (the
JWT branch decodes it, the API-key branch reads user.token_version), carry it
through gns3_ctx, and pass it at every minting call.
This commit is contained in:
YueGuobin 2026-07-15 22:42:24 +08:00
parent 89e11d265f
commit 3d6d9a3396
No known key found for this signature in database
4 changed files with 14 additions and 5 deletions

View File

@ -194,6 +194,12 @@ _jwt_token_var: contextvars.ContextVar[str | None] = contextvars.ContextVar(
_jwt_username_var: contextvars.ContextVar[str | None] = contextvars.ContextVar(
"mcp_jwt_username", default=None
)
# token_version extracted during token validation — short-lived JWTs minted for
# download/console URLs must carry the same version, or the revocation check
# (token_data.token_version != user.token_version) rejects them as "revoked".
_jwt_token_version_var: contextvars.ContextVar[int] = contextvars.ContextVar(
"mcp_jwt_token_version", default=0
)
# ── Token validation ──────────────────────────────────────────────────
@ -208,8 +214,9 @@ async def _resolve_token(token: str) -> str | None:
"""
# Try JWT first
try:
username = auth_service.get_username_from_token(token)
_jwt_username_var.set(username)
token_data = auth_service.get_token_data(token)
_jwt_username_var.set(token_data.username)
_jwt_token_version_var.set(token_data.token_version)
return token
except Exception:
pass
@ -233,6 +240,7 @@ async def _resolve_token(token: str) -> str | None:
user = await user_repo.get_user(db_key.user_id)
if user:
_jwt_username_var.set(user.username)
_jwt_token_version_var.set(user.token_version)
fresh_token = auth_service.create_access_token(user.username, token_version=user.token_version)
return fresh_token
except Exception:
@ -292,6 +300,7 @@ def _run_handler_sync(handler, params: dict[str, Any]) -> list[dict[str, Any]]:
"server_url": _server_url(),
"jwt_token": _jwt_token_var.get(),
"jwt_username": _jwt_username_var.get(),
"jwt_token_version": _jwt_token_version_var.get(),
}
result = handler(params, ctx)
return [{"type": "text", "text": json.dumps(result, ensure_ascii=False, default=str)}]

View File

@ -336,7 +336,7 @@ def download_capture_file_handler(params: dict[str, Any], gns3_ctx: dict[str, An
if not project_id:
return {"error": "project_id is required"}
username = gns3_ctx.get("jwt_username")
download_token = auth_service.create_access_token(username, expires_in=10) if username else None
download_token = auth_service.create_access_token(username, token_version=gns3_ctx.get("jwt_token_version", 0), expires_in=10) if username else None
link_ids = params.get("link_ids")
if link_ids:

View File

@ -316,7 +316,7 @@ def get_node_console_info_handler(params: dict[str, Any], gns3_ctx: dict[str, An
console_type = node.get("console_type", "unknown")
# Short-lived JWT for the WebSocket URL (10 min)
username = gns3_ctx.get("jwt_username")
ws_token = auth_service.create_access_token(username, expires_in=10) if username else None
ws_token = auth_service.create_access_token(username, token_version=gns3_ctx.get("jwt_token_version", 0), expires_in=10) if username else None
raw_url = f"{gns3_ctx['server_url']}/v3/projects/{project_id}/nodes/{node_id}/console/ws"
if ws_token:
raw_url += f"?token={ws_token}"

View File

@ -54,7 +54,7 @@ def get_symbol_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict
return {"error": "symbol_id is required"}
download_url = f"{gns3_ctx['server_url']}/v3/symbols/{symbol_id}/raw"
username = gns3_ctx.get("jwt_username")
download_token = auth_service.create_access_token(username, expires_in=10) if username else None
download_token = auth_service.create_access_token(username, token_version=gns3_ctx.get("jwt_token_version", 0), expires_in=10) if username else None
result = {
"symbol_id": symbol_id,
"download_url": download_url,