mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-12 22:14:05 +00:00
Key cached MCP clients by a digest of the whole token
The cache key held the first ten characters of a bearer token or API key. Every fine-grained GitHub token starts with github_pat_, so two users of the same server shared one cached client, and with it the first user's token, for up to five minutes.
This commit is contained in:
1 parent
f4397a7a65
commit
35d6d80e87
2 files changed
+30
-2
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import concurrent.futures
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
@@ -39,6 +40,16 @@ _mcp_clients_cache = {}
|
||||
_ANNOTATION_HINTS = ("readOnlyHint", "destructiveHint", "idempotentHint", "openWorldHint")
|
||||
|
||||
|
||||
def _secret_fingerprint(secret: str) -> str:
|
||||
"""A short digest of a whole secret, for telling cached clients apart.
|
||||
|
||||
A prefix of the secret itself is not enough: every fine-grained GitHub
|
||||
token starts with ``github_pat_``, so two users' tokens would share (and
|
||||
reuse) one cached client carrying the first user's token.
|
||||
"""
|
||||
return hashlib.sha256(secret.encode("utf-8")).hexdigest()[:16]
|
||||
|
||||
|
||||
def _annotation_hints(annotations: Any) -> Dict[str, bool]:
|
||||
"""The boolean MCP tool annotation hints, from a model or a dict."""
|
||||
if annotations is None:
|
||||
@@ -162,10 +173,10 @@ class MCPTool(Tool):
|
||||
token = self.auth_credentials.get(
|
||||
"bearer_token", ""
|
||||
) or self.auth_credentials.get("access_token", "")
|
||||
auth_key = f"bearer:{token[:10]}..." if token else "bearer:none"
|
||||
auth_key = f"bearer:{_secret_fingerprint(token)}" if token else "bearer:none"
|
||||
elif self.auth_type == "api_key":
|
||||
api_key = self.auth_credentials.get("api_key", "")
|
||||
auth_key = f"apikey:{api_key[:10]}..." if api_key else "apikey:none"
|
||||
auth_key = f"apikey:{_secret_fingerprint(api_key)}" if api_key else "apikey:none"
|
||||
elif self.auth_type == "basic":
|
||||
username = self.auth_credentials.get("username", "")
|
||||
auth_key = f"basic:{username}"
|
||||
|
||||
@@ -266,6 +266,23 @@ class TestGenerateCacheKey:
|
||||
})
|
||||
assert "apikey:none" in tool._cache_key
|
||||
|
||||
@pytest.mark.parametrize("auth_type, field", [("bearer", "bearer_token"), ("api_key", "api_key")])
|
||||
def test_tokens_with_a_shared_prefix_get_their_own_client(self, auth_type, field):
|
||||
"""Every fine-grained GitHub token starts with ``github_pat_``; two users' tokens
|
||||
must never share a cached client (it carries the first user's token)."""
|
||||
first = _make_tool({
|
||||
"server_url": "https://mcp.example.com",
|
||||
"auth_type": auth_type,
|
||||
"auth_credentials": {field: "github_pat_11AAAAAAA_alice"},
|
||||
})
|
||||
second = _make_tool({
|
||||
"server_url": "https://mcp.example.com",
|
||||
"auth_type": auth_type,
|
||||
"auth_credentials": {field: "github_pat_11AAAAAAA_bob"},
|
||||
})
|
||||
assert first._cache_key != second._cache_key
|
||||
assert "github_pat" not in first._cache_key
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# Transport Creation
|
||||
|
||||
Reference in new issue
Block a user