mirror of
https://github.com/mcp-use/mcp-use.git
synced 2026-09-14 18:06:01 +08:00
d789a92b4b
* Fix DNS rebinding protection for cloud/proxy deployments
- Add host/port parameters to MCPServer.__init__() and pass to FastMCP
- Default host to "0.0.0.0" (cloud-friendly, disables DNS protection)
- "127.0.0.1" auto-enables DNS protection (handled by FastMCP)
- run() can override host/port, falls back to __init__ values
- Add comprehensive docstring to __init__() explaining parameters
- Update documentation
This fixes 421 Misdirected Request errors when running behind reverse
proxies (Cloudflare, nginx, etc.) where the Host header doesn't match
the auto-configured allowed_hosts for localhost.
* Fix DNS rebinding protection for cloud/proxy deployments
- Add host/port parameters to MCPServer.__init__() and pass to FastMCP
- Default host to "0.0.0.0" (cloud-friendly, disables DNS protection)
- "127.0.0.1" auto-enables DNS protection (handled by FastMCP)
- run() can override host/port and reconfigures DNS protection accordingly
- Rebuild app when host changes in run() to apply new security settings
- Add comprehensive docstring to __init__() explaining parameters
- Add unit tests for host/port configuration and run() overrides
- Update documentation
This fixes 421 Misdirected Request errors when running behind reverse
proxies (Cloudflare, nginx, etc.) where the Host header doesn't match
the auto-configured allowed_hosts for localhost.
* ruff lint
* Bump mcp minimum version to 1.23.0
Required for TransportSecuritySettings and DNS rebinding protection
auto-configuration feature that was added in v1.23.0.
* feat(server): add bearer token authentication for Python MCP server
Add HTTP-layer bearer token authentication that returns proper 401 responses.
- BearerAuthProvider: abstract base class with verify_token() method
- AccessToken: model with token, claims, and scopes fields
- AuthMiddleware: Starlette middleware returning HTTP 401 for invalid tokens
- get_access_token() / require_auth(): context helpers for tools
Usage:
```python
class MyAuth(BearerAuthProvider):
async def verify_token(self, token: str) -> AccessToken | None:
if token != "secret":
return None
return AccessToken(token=token, claims={"user": "alice"})
server = MCPServer(name="my-server", auth=MyAuth())
```
Closes #840
* autogenerate docs
* test(server): add unit and integration tests for bearer token auth
Unit tests:
- AccessToken model creation and serialization
- BearerAuthProvider abstract class and subclassing
- get_access_token/require_auth context helpers
- AuthMiddleware 401 responses, path exclusion, CORS
Integration tests:
- MCPClient connecting to bearer auth server with valid token
- Connection failure with invalid/missing token
- Tool access to user claims and scopes
* fix(server): address code review feedback for bearer auth
- Fix CORS security: allow_credentials=False with allow_origins=["*"]
- Remove dead try/except import for auth module
- Remove PII (email) from debug logs
- Add comment explaining broad exception handling in verify_token
- Add /openmcp.json to docs excluded paths list
- Remove set_access_token from public API (internal only)
- Create AuthenticationError instead of using ValueError
- Skip middleware tests on older httpx versions (TestClient compat)
* docs: update API documentation for auth module
* fix(server): address Copilot review feedback
- Fix bearer.mdx: require_auth raises AuthenticationError, not ValueError
- Fix API docs: claims and scopes are optional with defaults, not required
- Remove deprecated SSE transport from bearer_auth_server.py test server
* autogenerate docs
* fix(server): address renvins review feedback
- Improve bearer.mdx claims documentation with explanation and examples
- Remove unnecessary cast() for AuthMiddleware in server.py
- Refactor auth tests to use pytest fixtures for cleaner setup/teardown
* docs(auth): clarify that all MCP endpoints are automatically protected
- Add "What Gets Protected" section explaining automatic protection
- Clarify that get_access_token/require_auth are for accessing token data, not protection
- Update examples to show proper usage pattern
- Improve Note at bottom to be clearer
* fix(auth): prevent path prefix bypass in auth middleware
Security fix: Use exact path matching or segment-based prefix matching
instead of simple startswith() to prevent auth bypass attacks.
Before: /docs-secret would bypass auth because it starts with /docs
After: Only /docs and /docs/* bypass auth, /docs-secret requires auth
Added tests for path matching security edge cases.
* refactor(auth): update import paths for authentication components
- Changed import statements in multiple documentation files and source files to reflect the new structure, moving authentication-related imports to the `auth` submodule.
- Updated examples to ensure clarity and consistency in usage patterns for `BearerAuthProvider`, `AccessToken`, `get_access_token`, and `require_auth`.
138 lines
5.0 KiB
Python
138 lines
5.0 KiB
Python
"""Authentication middleware for mcp-use server."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import TYPE_CHECKING
|
|
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
from starlette.requests import Request
|
|
from starlette.responses import JSONResponse, Response
|
|
|
|
from .dependencies import set_access_token
|
|
|
|
if TYPE_CHECKING:
|
|
from starlette.types import ASGIApp
|
|
|
|
from .bearer import BearerAuthProvider
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AuthMiddleware(BaseHTTPMiddleware):
|
|
"""Middleware that validates Bearer tokens on MCP endpoints.
|
|
|
|
This middleware:
|
|
1. Extracts Bearer token from Authorization header
|
|
2. Validates token with the configured BearerAuthProvider
|
|
3. Sets the AccessToken in context for tools to access via get_access_token()
|
|
4. Returns 401 for unauthenticated requests to protected paths
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
app: ASGIApp,
|
|
auth_provider: BearerAuthProvider,
|
|
exclude_paths: list[str] | None = None,
|
|
protected_paths: list[str] | None = None,
|
|
):
|
|
"""Initialize auth middleware.
|
|
|
|
Args:
|
|
app: The ASGI application
|
|
auth_provider: Provider to validate tokens
|
|
exclude_paths: Paths to exclude from authentication
|
|
protected_paths: Paths that require authentication (default: /mcp)
|
|
"""
|
|
super().__init__(app)
|
|
self.auth_provider = auth_provider
|
|
self.exclude_paths = exclude_paths or [
|
|
"/health",
|
|
"/docs",
|
|
"/inspector",
|
|
"/openmcp.json",
|
|
]
|
|
self.protected_paths = protected_paths or ["/mcp"]
|
|
|
|
def _is_protected_path(self, path: str) -> bool:
|
|
"""Check if the path requires authentication."""
|
|
for protected_path in self.protected_paths:
|
|
if path == protected_path or path.startswith(f"{protected_path}/"):
|
|
return True
|
|
return False
|
|
|
|
async def dispatch(self, request: Request, call_next) -> Response:
|
|
"""Process the request and validate authentication."""
|
|
# Always allow OPTIONS requests (CORS preflight)
|
|
if request.method == "OPTIONS":
|
|
set_access_token(None)
|
|
return await call_next(request)
|
|
|
|
# Check if path should be excluded from auth
|
|
path = request.url.path
|
|
for exclude_path in self.exclude_paths:
|
|
if path == exclude_path or path.startswith(f"{exclude_path}/"):
|
|
set_access_token(None)
|
|
return await call_next(request)
|
|
|
|
# Check if this is a protected path
|
|
is_protected = self._is_protected_path(path)
|
|
|
|
# Extract Bearer token from Authorization header
|
|
auth_header = request.headers.get("Authorization", "")
|
|
|
|
if auth_header.startswith("Bearer "):
|
|
token = auth_header[7:]
|
|
|
|
try:
|
|
# Validate token with provider
|
|
access_token = await self.auth_provider.verify_token(token)
|
|
|
|
if access_token is not None:
|
|
set_access_token(access_token)
|
|
logger.debug("Authenticated request")
|
|
else:
|
|
# Token validation failed
|
|
set_access_token(None)
|
|
if is_protected:
|
|
logger.debug("Token validation failed for protected path")
|
|
return JSONResponse(
|
|
status_code=401,
|
|
content={
|
|
"error": "invalid_token",
|
|
"error_description": "Token is invalid or expired",
|
|
},
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
except Exception:
|
|
# Catch exceptions from user-implemented verify_token()
|
|
# Log for debugging but return 401 to avoid leaking error details
|
|
logger.warning("Token validation raised an exception", exc_info=True)
|
|
set_access_token(None)
|
|
if is_protected:
|
|
return JSONResponse(
|
|
status_code=401,
|
|
content={
|
|
"error": "invalid_token",
|
|
"error_description": "Token validation failed",
|
|
},
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
else:
|
|
# No auth header present
|
|
set_access_token(None)
|
|
if is_protected:
|
|
logger.debug(f"Unauthenticated request to protected path: {path}")
|
|
return JSONResponse(
|
|
status_code=401,
|
|
content={
|
|
"error": "unauthorized",
|
|
"error_description": "Authentication required",
|
|
},
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
response = await call_next(request)
|
|
return response
|