Files
mnemosyne/mnemosyne/mcp_server/tests/test_auth.py
Robert Helewka 81426327bf 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.
2026-04-27 09:01:36 -04:00

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}",
)