TASK-022: Hash session tokens in database for security

- Store session tokens as one-way SHA256 hashes instead of plaintext
- Hash tokens on write (create_session) and on read (get_session, delete_session)
- Add migration to drop plaintext sessions table and recreate with token_hash column
- Update Session model: token field still contains raw token for signing
- Add test to verify tokens are hashed in database, not plaintext
- Update Architekture.md to document session token hashing
- Update Backend-Development.md with implementation pattern and best practices

Prevents direct session token hijacking if database file is exposed to attacker.
If plaintext DB was readable, sessions are invalidated by the migration anyway.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-26 14:35:32 +02:00
parent 5709785942
commit 81f009e323
7 changed files with 168 additions and 45 deletions

View File

@@ -80,7 +80,7 @@ async def test_init_db_records_schema_version(tmp_path: Path) -> None:
) as cursor:
row = await cursor.fetchone()
assert row is not None
assert row[0] == 1
assert row[0] == 2
@pytest.mark.asyncio
@@ -97,4 +97,4 @@ async def test_init_db_migrates_legacy_database_without_schema_version(tmp_path:
) as cursor:
row = await cursor.fetchone()
assert row is not None
assert row[0] == 1
assert row[0] == 2

View File

@@ -116,3 +116,28 @@ class TestSessionRepo:
assert deleted == 1
assert await session_repo.get_session(db, "expired") is None
assert await session_repo.get_session(db, "valid") is not None
async def test_tokens_are_hashed_in_database(
self, db: aiosqlite.Connection
) -> None:
"""Verify that tokens are stored as hashes, not plaintext."""
import hashlib
token = "plaintext_token_12345"
await session_repo.create_session(
db,
token=token,
created_at="2025-01-01T00:00:00+00:00",
expires_at="2025-01-01T01:00:00+00:00",
)
# Query the database directly to verify the token is hashed.
async with db.execute("SELECT token_hash FROM sessions WHERE token_hash = ?", (hashlib.sha256(token.encode()).hexdigest(),)) as cursor:
row = await cursor.fetchone()
assert row is not None
assert row[0] == hashlib.sha256(token.encode()).hexdigest()
# The plaintext token should not be in the database.
async with db.execute("SELECT * FROM sessions WHERE token_hash = ?", (token,)) as cursor:
row = await cursor.fetchone()
assert row is None