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.
71 lines
2.3 KiB
Python
71 lines
2.3 KiB
Python
"""Tests for resolve_mcp_user."""
|
|
|
|
from datetime import timedelta
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.test import TestCase
|
|
from django.utils import timezone
|
|
|
|
from mcp_server.auth import MCPAuthError, resolve_mcp_user
|
|
from mcp_server.models import MCPToken
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
class ResolveMCPUserTest(TestCase):
|
|
def setUp(self):
|
|
self.user = User.objects.create_user(
|
|
username="bob", email="bob@example.com", password="pw"
|
|
)
|
|
self.token, self.plaintext = MCPToken.objects.create_token(
|
|
user=self.user, name="t"
|
|
)
|
|
|
|
def test_resolves_valid_token(self):
|
|
user, token = resolve_mcp_user(self.plaintext)
|
|
self.assertEqual(user.pk, self.user.pk)
|
|
self.assertEqual(token.pk, self.token.pk)
|
|
|
|
def test_records_usage(self):
|
|
self.assertIsNone(self.token.last_used_at)
|
|
resolve_mcp_user(self.plaintext)
|
|
self.token.refresh_from_db()
|
|
self.assertIsNotNone(self.token.last_used_at)
|
|
|
|
def test_invalid_token_raises(self):
|
|
with self.assertRaises(MCPAuthError):
|
|
resolve_mcp_user("not-a-real-token")
|
|
|
|
def test_inactive_token_raises(self):
|
|
self.token.is_active = False
|
|
self.token.save()
|
|
with self.assertRaises(MCPAuthError):
|
|
resolve_mcp_user(self.plaintext)
|
|
|
|
def test_expired_token_raises(self):
|
|
self.token.expires_at = timezone.now() - timedelta(hours=1)
|
|
self.token.save()
|
|
with self.assertRaises(MCPAuthError):
|
|
resolve_mcp_user(self.plaintext)
|
|
|
|
def test_disabled_user_raises(self):
|
|
self.user.is_active = False
|
|
self.user.save()
|
|
with self.assertRaises(MCPAuthError):
|
|
resolve_mcp_user(self.plaintext)
|
|
|
|
def test_plaintext_not_in_db(self):
|
|
# Defense in depth: scan every column for the plaintext value.
|
|
from django.db import connection
|
|
|
|
plaintext = self.plaintext
|
|
with connection.cursor() as cur:
|
|
cur.execute("SELECT * FROM mcp_server_mcptoken")
|
|
rows = cur.fetchall()
|
|
for row in rows:
|
|
for value in row:
|
|
self.assertNotEqual(
|
|
value, plaintext,
|
|
f"Plaintext token leaked into the database: {value!r}",
|
|
)
|