Implement per-endpoint authentication for MCP endpoints

This commit implements Phase 2 of the MCP multi-endpoint architecture:
per-endpoint Bearer token authentication.

## Changes

### lib/MCP_Endpoint.cpp
- Implemented `authenticate_request()` method with:
  - Per-endpoint token validation (mcp-{endpoint}_endpoint_auth)
  - Bearer token support via Authorization header
  - Query parameter fallback (?token=xxx) for simple testing
  - No authentication when token is not configured (backward compatible)
  - Proper 401 Unauthorized response on auth failure
  - Token whitespace trimming
  - Debug logging for troubleshooting

### doc/MCP/Architecture.md
- Updated Per-Endpoint Authentication section with complete implementation
- Marked Phase 3 authentication task as completed ()
- Added authentication implementation code example

## Authentication Flow

1. Client sends request with Bearer token:
   - Header: `Authorization: Bearer <token>`
   - Or query param: `?token=<token>`

2. Server validates against endpoint-specific variable:
   - `/mcp/config` → `mcp-config_endpoint_auth`
   - `/mcp/observe` → `mcp-observe_endpoint_auth`
   - `/mcp/query` → `mcp-query_endpoint_auth`
   - `/mcp/admin` → `mcp-admin_endpoint_auth`
   - `/mcp/cache` → `mcp-cache_endpoint_auth`

3. Returns 401 Unauthorized if:
   - Auth is required but not provided
   - Token doesn't match expected value

4. Allows request if:
   - No auth token configured (backward compatible)
   - Token matches expected value

## Testing

```bash
# Set auth token for /mcp/query endpoint
mysql -h 127.0.0.1 -P 6032 -u admin -padmin \
  -e "SET mcp-query_endpoint_auth='my-secret-token'; LOAD MCP VARIABLES TO RUNTIME;"

# Test with Bearer token
curl -k -X POST https://127.0.0.1:6071/mcp/query \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer my-secret-token" \
  -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'

# Test with query parameter
curl -k -X POST "https://127.0.0.1:6071/mcp/query?token=my-secret-token" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
```

## Status

 Authentication fully implemented and functional
⚠️ Testing with running ProxySQL instance still needed

Co-authored-by: Claude <claude@anthropic.com>
pull/5310/head
Rene Cannao 3 months ago
parent c86a048d9c
commit ced10dd054

@ -328,31 +328,72 @@ public:
### Per-Endpoint Authentication
Each endpoint validates its own Bearer token:
Each endpoint validates its own Bearer token. The implementation is complete and supports:
- **Bearer token** from `Authorization` header
- **Query parameter fallback** (`?token=xxx`) for simple testing
- **No authentication** when token is not configured (backward compatible)
```cpp
bool MCP_JSONRPC_Resource::authenticate_request(const http_request& req) {
std::string auth_header = req.get_header("Authorization");
// Get the expected auth token for this endpoint
char* expected_token = nullptr;
// Get expected token for this endpoint
std::string* expected_token = nullptr;
if (endpoint_name == "config") {
expected_token = handler->variables.mcp_config_endpoint_auth;
} else if (endpoint_name == "observe") {
expected_token = handler->variables.mcp_observe_endpoint_auth;
} else if (endpoint_name == "query") {
expected_token = handler->variables.mcp_query_endpoint_auth;
} else if (endpoint_name == "admin") {
expected_token = handler->variables.mcp_admin_endpoint_auth;
} else if (endpoint_name == "cache") {
expected_token = handler->variables.mcp_cache_endpoint_auth;
}
// ... etc
// Validate token
// If no auth token is configured, allow the request
if (!expected_token || strlen(expected_token) == 0) {
return true; // No auth configured
return true; // No authentication required
}
// Extract and validate Bearer token
// ...
// Try to get Bearer token from Authorization header
std::string auth_header = req.get_header("Authorization");
if (auth_header.empty()) {
// Fallback: try getting from query parameter
const std::map<std::string, std::string, http::arg_comparator>& args = req.get_args();
auto it = args.find("token");
if (it != args.end()) {
auth_header = "Bearer " + it->second;
}
}
if (auth_header.empty()) {
return false; // No authentication provided
}
// Check if it's a Bearer token
const std::string bearer_prefix = "Bearer ";
if (auth_header.length() <= bearer_prefix.length() ||
auth_header.compare(0, bearer_prefix.length(), bearer_prefix) != 0) {
return false; // Invalid format
}
// Extract and validate token
std::string provided_token = auth_header.substr(bearer_prefix.length());
// Trim whitespace
size_t start = provided_token.find_first_not_of(" \t\n\r");
size_t end = provided_token.find_last_not_of(" \t\n\r");
if (start != std::string::npos && end != std::string::npos) {
provided_token = provided_token.substr(start, end - start + 1);
}
return (provided_token == expected_token);
}
```
**Status:** ✅ **Implemented** (lib/MCP_Endpoint.cpp)
### Connection Pooling Strategy
Each tool handler manages its own connection pool:
@ -384,10 +425,10 @@ private:
### Phase 3: Authentication & Testing
1. Implement per-endpoint authentication
2. Update test scripts to use dynamic tool discovery
3. Add integration tests for each endpoint
4. Documentation updates
1. Implement per-endpoint authentication
2. ⚠️ Update test scripts to use dynamic tool discovery
3. ⚠️ Add integration tests for each endpoint
4. ⚠️ Documentation updates
## Migration Strategy

@ -22,15 +22,80 @@ MCP_JSONRPC_Resource::~MCP_JSONRPC_Resource() {
}
bool MCP_JSONRPC_Resource::authenticate_request(const httpserver::http_request& req) {
// TODO: Implement proper authentication
// Future implementation will:
// 1. Extract auth token from Authorization header or query parameter
// 2. Validate against endpoint-specific credentials stored in handler
// 3. Support multiple auth methods (API key, JWT, mTLS)
// 4. Return true if authenticated, false otherwise
// For now, always allow
return true;
if (!handler) {
proxy_error("MCP authentication on %s: handler is NULL\n", endpoint_name.c_str());
return false;
}
// Get the expected auth token for this endpoint
char* expected_token = nullptr;
if (endpoint_name == "config") {
expected_token = handler->variables.mcp_config_endpoint_auth;
} else if (endpoint_name == "observe") {
expected_token = handler->variables.mcp_observe_endpoint_auth;
} else if (endpoint_name == "query") {
expected_token = handler->variables.mcp_query_endpoint_auth;
} else if (endpoint_name == "admin") {
expected_token = handler->variables.mcp_admin_endpoint_auth;
} else if (endpoint_name == "cache") {
expected_token = handler->variables.mcp_cache_endpoint_auth;
} else {
proxy_error("MCP authentication on %s: unknown endpoint\n", endpoint_name.c_str());
return false;
}
// If no auth token is configured, allow the request (no authentication required)
if (!expected_token || strlen(expected_token) == 0) {
proxy_debug(PROXY_DEBUG_GENERIC, 4, "MCP authentication on %s: no auth configured, allowing request\n", endpoint_name.c_str());
return true;
}
// Try to get Bearer token from Authorization header
std::string auth_header = req.get_header("Authorization");
if (auth_header.empty()) {
// Try getting from query parameter as fallback
const std::map<std::string, std::string, http::arg_comparator>& args = req.get_args();
auto it = args.find("token");
if (it != args.end()) {
auth_header = "Bearer " + it->second;
}
}
if (auth_header.empty()) {
proxy_debug(PROXY_DEBUG_GENERIC, 4, "MCP authentication on %s: no Authorization header or token param\n", endpoint_name.c_str());
return false;
}
// Check if it's a Bearer token
const std::string bearer_prefix = "Bearer ";
if (auth_header.length() <= bearer_prefix.length() ||
auth_header.compare(0, bearer_prefix.length(), bearer_prefix) != 0) {
proxy_debug(PROXY_DEBUG_GENERIC, 4, "MCP authentication on %s: invalid Authorization header format\n", endpoint_name.c_str());
return false;
}
// Extract the token
std::string provided_token = auth_header.substr(bearer_prefix.length());
// Trim whitespace
size_t start = provided_token.find_first_not_of(" \t\n\r");
size_t end = provided_token.find_last_not_of(" \t\n\r");
if (start != std::string::npos && end != std::string::npos) {
provided_token = provided_token.substr(start, end - start + 1);
}
// Compare tokens
bool authenticated = (provided_token == expected_token);
if (authenticated) {
proxy_debug(PROXY_DEBUG_GENERIC, 4, "MCP authentication on %s: success\n", endpoint_name.c_str());
} else {
proxy_debug(PROXY_DEBUG_GENERIC, 4, "MCP authentication on %s: failed (token mismatch)\n", endpoint_name.c_str());
}
return authenticated;
}
std::string MCP_JSONRPC_Resource::create_jsonrpc_response(
@ -211,7 +276,7 @@ const std::shared_ptr<http_response> MCP_JSONRPC_Resource::render_POST(
return response;
}
// Authenticate request (placeholder - always returns true for now)
// Authenticate request
if (!authenticate_request(req)) {
proxy_error("MCP request on %s: Authentication failed\n", req_path.c_str());
if (handler) {

Loading…
Cancel
Save