feat(mcp): store MCP tokens as SHA-256 hashes instead of plaintext
Replace plaintext token storage with SHA-256 hashes so leaked database contents cannot be used to authenticate. Plaintext is generated, shown once at creation time, and never persisted. - Add `hash_token()` helper and `MCPTokenManager.create_token()` that returns `(instance, plaintext)`. - Replace `token` field with indexed `token_hash`; look up bearers by hashing the incoming value. - Update dashboard, management command, and admin to surface plaintext only at creation. Disable admin "add" since it cannot reveal plaintext. - Migration drops the old `token` column and adds `token_hash`; pre-existing tokens are invalidated and must be reissued.
This commit is contained in:
@@ -12,7 +12,7 @@ from fastmcp.server.dependencies import get_http_request
|
||||
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
||||
|
||||
from .metrics import mcp_auth_failures_total
|
||||
from .models import MCPToken
|
||||
from .models import MCPToken, hash_token
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -25,9 +25,17 @@ class MCPAuthError(Exception):
|
||||
|
||||
|
||||
def resolve_mcp_user(token_string: str):
|
||||
"""Resolve a bearer token to (user, MCPToken). Raises MCPAuthError on any failure."""
|
||||
"""Resolve a bearer token to (user, MCPToken). Raises MCPAuthError on any failure.
|
||||
|
||||
Hashes the incoming bearer and looks up by the hash — plaintext is never
|
||||
stored or compared directly.
|
||||
"""
|
||||
try:
|
||||
token = MCPToken.objects.select_related("user").get(token=token_string)
|
||||
token = (
|
||||
MCPToken.objects
|
||||
.select_related("user")
|
||||
.get(token_hash=hash_token(token_string))
|
||||
)
|
||||
except MCPToken.DoesNotExist:
|
||||
raise MCPAuthError("Invalid MCP token.")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user