This is an automated email from the ASF dual-hosted git repository.
FreeOnePlus pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-mcp-server.git
The following commit(s) were added to refs/heads/master by this push:
new ddb1b5d fix: require headers for token management auth (#112)
ddb1b5d is described below
commit ddb1b5d4395752c8768c4158ad7eabceb3775112
Author: Yijia Su <[email protected]>
AuthorDate: Wed Jul 29 19:31:51 2026 +0800
fix: require headers for token management auth (#112)
---
CHANGELOG.md | 2 +
README.md | 13 +-
doris_mcp_server/auth/token_handlers.py | 37 +++--
doris_mcp_server/auth/token_security_middleware.py | 8 +-
test/security/test_token_management_auth.py | 160 +++++++++++++++++++++
5 files changed, 190 insertions(+), 30 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 653a9ee..fc211d7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -48,6 +48,8 @@ under **Unreleased** until a new version is selected and
published.
- Preserved service availability after malformed requests, unknown methods,
header mismatches, unsupported versions, and missing capabilities.
- Corrected resource and prompt error semantics and tool `isError` results.
+- Removed admin-token query authentication and dashboard URL propagation;
+ management endpoints now accept admin credentials only in headers.
- Released Doris connections on SQL profile, data freshness, and access
analysis paths.
- Improved Doris 4 role metadata compatibility and query recovery behavior.
diff --git a/README.md b/README.md
index b8546d2..a2574a1 100644
--- a/README.md
+++ b/README.md
@@ -143,11 +143,20 @@ Access the **Web-Based Token Management Dashboard** for
enterprise-grade token a
```
#### **Interface Access**
+
+Management requests accept the admin token only in an HTTP header. Query-string
+tokens are rejected and must not be placed in URLs, browser history, or access
+logs.
+
```bash
-# Access the token management interface
-http://localhost:3000/token/management?admin_token=your_secure_admin_token
+curl -H "Authorization: Bearer your_secure_admin_token" \
+ http://127.0.0.1:3000/token/stats
```
+The optional `/token/management` page must be opened through a client or local
+proxy that supplies the same header. Its API requests use an in-page password
+field and do not persist or propagate the token in URLs.
+
#### **Available Operations**
- **📊 Token Statistics**: Real-time overview of active, expired, and total
tokens
- **➕ Create Tokens**:
diff --git a/doris_mcp_server/auth/token_handlers.py
b/doris_mcp_server/auth/token_handlers.py
index cd16161..06a4743 100644
--- a/doris_mcp_server/auth/token_handlers.py
+++ b/doris_mcp_server/auth/token_handlers.py
@@ -410,6 +410,15 @@ class TokenHandlers:
<body>
<div class="container">
<h1>🔐 Doris MCP Server - Token Management</h1>
+
+ <div class="section">
+ <h2>🛡️ Admin Authentication</h2>
+ <div class="form-group">
+ <label for="adminToken">Admin Token:</label>
+ <input type="password" id="adminToken"
autocomplete="off" placeholder="Enter the configured admin token">
+ <small style="color: #666; display: block;
margin-top: 5px;">The token stays in this page and is sent only in the
Authorization header.</small>
+ </div>
+ </div>
<div class="section">
<h2>📊 Token Statistics</h2>
@@ -517,12 +526,9 @@ class TokenHandlers:
</div>
<script>
- // Get admin token from URL parameters
- const urlParams = new
URLSearchParams(window.location.search);
- const adminToken = urlParams.get('admin_token');
-
- // Create request headers with admin token
+ // Read the token only from the in-page password field.
function getAuthHeaders() {{
+ const adminToken =
document.getElementById('adminToken').value;
if (adminToken) {{
return {{
'Content-Type': 'application/json',
@@ -533,15 +539,6 @@ class TokenHandlers:
}}
}}
- // Create URL with admin token parameter
- function getAuthURL(baseUrl) {{
- if (adminToken) {{
- const separator = baseUrl.includes('?') ? '&' :
'?';
- return
`${{baseUrl}}${{separator}}admin_token=${{encodeURIComponent(adminToken)}}`;
- }}
- return baseUrl;
- }}
-
function showResponse(elementId, data, isSuccess = true) {{
const element = document.getElementById(elementId);
element.innerHTML = '<pre>' + JSON.stringify(data,
null, 2) + '</pre>';
@@ -580,7 +577,7 @@ class TokenHandlers:
delete data.db_fe_http_port;
try {{
- const response = await
fetch(getAuthURL('/token/create'), {{
+ const response = await fetch('/token/create', {{
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(data)
@@ -600,7 +597,7 @@ class TokenHandlers:
// List tokens
document.getElementById('listTokensBtn').addEventListener('click', async () =>
{{
try {{
- const response = await
fetch(getAuthURL('/token/list'), {{
+ const response = await fetch('/token/list', {{
headers: getAuthHeaders()
}});
const result = await response.json();
@@ -613,7 +610,7 @@ class TokenHandlers:
// Cleanup tokens
document.getElementById('cleanupTokensBtn').addEventListener('click', async ()
=> {{
try {{
- const response = await
fetch(getAuthURL('/token/cleanup'), {{
+ const response = await fetch('/token/cleanup', {{
method: 'POST',
headers: getAuthHeaders()
}});
@@ -633,7 +630,7 @@ class TokenHandlers:
}}
try {{
- const response = await
fetch(getAuthURL(`/token/revoke?token_id=${{encodeURIComponent(tokenId)}}`), {{
+ const response = await
fetch(`/token/revoke?token_id=${{encodeURIComponent(tokenId)}}`, {{
method: 'DELETE',
headers: getAuthHeaders()
}});
@@ -650,8 +647,6 @@ class TokenHandlers:
}}
}});
- // Load token list on page load
- document.getElementById('listTokensBtn').click();
</script>
</body>
</html>
@@ -674,4 +669,4 @@ class TokenHandlers:
</body>
</html>
"""
- return HTMLResponse(error_html, status_code=500)
\ No newline at end of file
+ return HTMLResponse(error_html, status_code=500)
diff --git a/doris_mcp_server/auth/token_security_middleware.py
b/doris_mcp_server/auth/token_security_middleware.py
index d38f17d..d9dbc3b 100644
--- a/doris_mcp_server/auth/token_security_middleware.py
+++ b/doris_mcp_server/auth/token_security_middleware.py
@@ -125,12 +125,6 @@ class TokenSecurityMiddleware:
if admin_token:
return admin_token
- # Try query parameter as fallback (not recommended for production)
- admin_token = request.query_params.get('admin_token', '')
- if admin_token:
- self.logger.warning("Admin token passed via query parameter - this
is insecure for production")
- return admin_token
-
return None
def _verify_admin_token(self, provided_token: str) -> bool:
@@ -224,4 +218,4 @@ class TokenSecurityMiddleware:
# Convenience function for middleware creation
def create_token_security_middleware(config: DorisConfig) ->
TokenSecurityMiddleware:
"""Create token security middleware with configuration"""
- return TokenSecurityMiddleware(config)
\ No newline at end of file
+ return TokenSecurityMiddleware(config)
diff --git a/test/security/test_token_management_auth.py
b/test/security/test_token_management_auth.py
new file mode 100644
index 0000000..79ef623
--- /dev/null
+++ b/test/security/test_token_management_auth.py
@@ -0,0 +1,160 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from urllib.parse import urlencode
+
+import httpx
+import pytest
+from starlette.applications import Starlette
+from starlette.requests import Request
+from starlette.routing import Route
+
+from doris_mcp_server.auth.token_handlers import TokenHandlers
+from doris_mcp_server.auth.token_security_middleware import
TokenSecurityMiddleware
+from doris_mcp_server.utils.config import DorisConfig
+
+ADMIN_TOKEN = "test-admin-token"
+
+
+def _config() -> DorisConfig:
+ config = DorisConfig()
+ config.security.enable_http_token_management = True
+ config.security.require_admin_auth = True
+ config.security.token_management_admin_token = ADMIN_TOKEN
+ config.security.token_management_allowed_ips = ["127.0.0.1"]
+ return config
+
+
+def _request(
+ path: str = "/token/stats",
+ *,
+ headers: dict[str, str] | None = None,
+ query: dict[str, str] | None = None,
+) -> Request:
+ encoded_headers = [
+ (name.lower().encode(), value.encode())
+ for name, value in (headers or {}).items()
+ ]
+ return Request(
+ {
+ "type": "http",
+ "method": "GET",
+ "scheme": "http",
+ "server": ("127.0.0.1", 3000),
+ "client": ("127.0.0.1", 50000),
+ "path": path,
+ "raw_path": path.encode(),
+ "query_string": urlencode(query or {}).encode(),
+ "headers": encoded_headers,
+ }
+ )
+
+
[email protected]
+async def test_admin_query_token_is_rejected_and_headers_are_accepted():
+ middleware = TokenSecurityMiddleware(_config())
+
+ query_only = await middleware.check_token_management_access(
+ _request(query={"admin_token": ADMIN_TOKEN})
+ )
+ assert query_only is not None
+ assert query_only.status_code == 401
+
+ bad_header_with_query = await middleware.check_token_management_access(
+ _request(
+ headers={"Authorization": "Bearer wrong-token"},
+ query={"admin_token": ADMIN_TOKEN},
+ )
+ )
+ assert bad_header_with_query is not None
+ assert bad_header_with_query.status_code == 401
+
+ assert (
+ await middleware.check_token_management_access(
+ _request(headers={"Authorization": f"Bearer {ADMIN_TOKEN}"})
+ )
+ is None
+ )
+ assert (
+ await middleware.check_token_management_access(
+ _request(headers={"X-Admin-Token": ADMIN_TOKEN})
+ )
+ is None
+ )
+
+
+class _AuthProvider:
+ token_manager = object()
+
+
+class _SecurityManager:
+ auth_provider = _AuthProvider()
+
+ @staticmethod
+ def get_token_stats():
+ return {
+ "total_tokens": 0,
+ "active_tokens": 0,
+ "expired_tokens": 0,
+ "expiry_enabled": True,
+ "default_expiry_hours": 24,
+ }
+
+
[email protected]
+async def test_http_management_route_requires_admin_header():
+ handlers = TokenHandlers(_SecurityManager(), _config())
+ app = Starlette(
+ routes=[Route("/token/stats", handlers.handle_token_stats,
methods=["GET"])]
+ )
+
+ async with httpx.AsyncClient(
+ transport=httpx.ASGITransport(app=app),
+ base_url="http://127.0.0.1:3000",
+ ) as client:
+ denied = await client.get(
+ "/token/stats",
+ params={"admin_token": ADMIN_TOKEN},
+ )
+ accepted = await client.get(
+ "/token/stats",
+ headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
+ )
+
+ assert denied.status_code == 401
+ assert accepted.status_code == 200
+ assert accepted.json()["success"] is True
+
+
[email protected]
+async def test_management_page_uses_header_only_and_never_builds_token_urls():
+ handlers = TokenHandlers(_SecurityManager(), _config())
+
+ response = await handlers.handle_management_page(
+ _request(
+ path="/token/management",
+ headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
+ )
+ )
+ body = response.body.decode()
+
+ assert response.status_code == 200
+ assert "admin_token" not in body
+ assert "URLSearchParams" not in body
+ assert "getAuthURL" not in body
+ assert "Authorization" in body
+ assert 'type="password" id="adminToken"' in body
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]