diff --git a/api/src/cartsnitch_api/auth/dependencies.py b/api/src/cartsnitch_api/auth/dependencies.py index 91c438f..4eac005 100644 --- a/api/src/cartsnitch_api/auth/dependencies.py +++ b/api/src/cartsnitch_api/auth/dependencies.py @@ -4,6 +4,7 @@ Validates Better-Auth session tokens from cookies or Bearer header. Sessions are verified by querying the shared sessions table directly. """ +import hashlib from datetime import UTC, datetime from fastapi import Cookie, Depends, Header, HTTPException, Request, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer @@ -26,11 +27,14 @@ SECURE_SESSION_COOKIE_NAME = "__Secure-better-auth.session_token" async def _validate_session_token(token: str, db: AsyncSession) -> str: """Validate a Better-Auth session token against the sessions table. - Returns the user_id (as str) if the session is valid and not expired. + Better-Auth v1.2+ stores SHA-256(raw_token) in the DB. + The cookie/Bearer header carries the raw token, so we hash before lookup. """ + token_hash = hashlib.sha256(token.encode()).hexdigest() + result = await db.execute( text("SELECT user_id, expires_at FROM sessions WHERE token = :token"), - {"token": token}, + {"token": token_hash}, ) row = result.first() diff --git a/api/tests/conftest.py b/api/tests/conftest.py index 61810e1..647dbd9 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -4,6 +4,7 @@ Session-based auth: tests create users and sessions directly in the DB, matching the Better-Auth session validation flow. """ +import hashlib import secrets import uuid from datetime import UTC, datetime, timedelta @@ -136,12 +137,14 @@ async def client(db_engine): async def _create_test_user_and_session(client: AsyncClient, db_engine, **user_overrides) -> tuple[dict, str]: """Create a test user and a valid session directly in the DB. - Returns (user_dict, session_token). + Returns (user_dict, session_token). Better-Auth v1.2+ stores SHA-256 + hashed tokens in the DB, so the token is hashed before insertion. """ user_id = str(uuid.uuid4()) email = user_overrides.get("email", "test@example.com") display_name = user_overrides.get("display_name", "Test User") session_token = secrets.token_urlsafe(32) + token_hash = hashlib.sha256(session_token.encode()).hexdigest() session_id = str(uuid.uuid4()) now = datetime.now(UTC).isoformat() expires = (datetime.now(UTC) + timedelta(days=7)).isoformat() @@ -169,7 +172,7 @@ async def _create_test_user_and_session(client: AsyncClient, db_engine, **user_o ), { "id": session_id, - "token": session_token, + "token": token_hash, "user_id": user_id, "expires_at": expires, "created_at": now, diff --git a/api/tests/test_auth/test_auth_endpoints.py b/api/tests/test_auth/test_auth_endpoints.py index 7b096ae..83e49d7 100644 --- a/api/tests/test_auth/test_auth_endpoints.py +++ b/api/tests/test_auth/test_auth_endpoints.py @@ -74,6 +74,7 @@ async def test_delete_me(client, auth_headers): @pytest.mark.asyncio async def test_expired_session_rejected(client, db_engine): """Expired sessions must be rejected.""" + import hashlib import secrets import uuid from datetime import UTC, datetime, timedelta @@ -82,6 +83,7 @@ async def test_expired_session_rejected(client, db_engine): user_id = str(uuid.uuid4()) session_token = secrets.token_urlsafe(32) + token_hash = hashlib.sha256(session_token.encode()).hexdigest() now = datetime.now(UTC).isoformat() expired = (datetime.now(UTC) - timedelta(hours=1)).isoformat() @@ -108,7 +110,7 @@ async def test_expired_session_rejected(client, db_engine): ), { "id": str(uuid.uuid4()), - "token": session_token, + "token": token_hash, "uid": user_id, "ea": expired, "ca": now,