This is an automated email from the ASF dual-hosted git repository.

FreeOnePlus pushed a commit to branch agent/sec-003-admin-token-header
in repository https://gitbox.apache.org/repos/asf/doris-mcp-server.git

commit 9572cfb7758bfe90f4f61cd84477c2dd89288266
Author: FreeOnePlus <[email protected]>
AuthorDate: Wed Jul 29 19:30:04 2026 +0800

    fix: require headers for token management auth
---
 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]

Reply via email to