Merge pull request #139 from cartsnitch/betty/revert-sha256-session-hash

fix(api): revert SHA-256 session token hashing — better-auth stores raw tokens
This commit is contained in:
cartsnitch-cto[bot]
2026-04-04 19:25:23 +00:00
committed by GitHub
3 changed files with 7 additions and 14 deletions
+3 -6
View File
@@ -4,7 +4,6 @@ Validates Better-Auth session tokens from cookies or Bearer header.
Sessions are verified by querying the shared sessions table directly. Sessions are verified by querying the shared sessions table directly.
""" """
import hashlib
from datetime import UTC, datetime from datetime import UTC, datetime
from fastapi import Cookie, Depends, Header, HTTPException, Request, status from fastapi import Cookie, Depends, Header, HTTPException, Request, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
@@ -27,14 +26,12 @@ SECURE_SESSION_COOKIE_NAME = "__Secure-better-auth.session_token"
async def _validate_session_token(token: str, db: AsyncSession) -> str: async def _validate_session_token(token: str, db: AsyncSession) -> str:
"""Validate a Better-Auth session token against the sessions table. """Validate a Better-Auth session token against the sessions table.
Better-Auth v1.2+ stores SHA-256(raw_token) in the DB. Better-Auth stores the raw token in the DB. The cookie/Bearer header
The cookie/Bearer header carries the raw token, so we hash before lookup. carries the same raw token, so we compare directly.
""" """
token_hash = hashlib.sha256(token.encode()).hexdigest()
result = await db.execute( result = await db.execute(
text("SELECT user_id, expires_at FROM sessions WHERE token = :token"), text("SELECT user_id, expires_at FROM sessions WHERE token = :token"),
{"token": token_hash}, {"token": token},
) )
row = result.first() row = result.first()
+3 -5
View File
@@ -4,7 +4,6 @@ Session-based auth: tests create users and sessions directly in the DB,
matching the Better-Auth session validation flow. matching the Better-Auth session validation flow.
""" """
import hashlib
import secrets import secrets
import uuid import uuid
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
@@ -137,14 +136,13 @@ async def client(db_engine):
async def _create_test_user_and_session(client: AsyncClient, db_engine, **user_overrides) -> tuple[dict, str]: 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. """Create a test user and a valid session directly in the DB.
Returns (user_dict, session_token). Better-Auth v1.2+ stores SHA-256 Returns (user_dict, session_token). Better-Auth stores the raw token
hashed tokens in the DB, so the token is hashed before insertion. in the DB, so we insert it as-is.
""" """
user_id = str(uuid.uuid4()) user_id = str(uuid.uuid4())
email = user_overrides.get("email", "test@example.com") email = user_overrides.get("email", "test@example.com")
display_name = user_overrides.get("display_name", "Test User") display_name = user_overrides.get("display_name", "Test User")
session_token = secrets.token_urlsafe(32) session_token = secrets.token_urlsafe(32)
token_hash = hashlib.sha256(session_token.encode()).hexdigest()
session_id = str(uuid.uuid4()) session_id = str(uuid.uuid4())
now = datetime.now(UTC).isoformat() now = datetime.now(UTC).isoformat()
expires = (datetime.now(UTC) + timedelta(days=7)).isoformat() expires = (datetime.now(UTC) + timedelta(days=7)).isoformat()
@@ -172,7 +170,7 @@ async def _create_test_user_and_session(client: AsyncClient, db_engine, **user_o
), ),
{ {
"id": session_id, "id": session_id,
"token": token_hash, "token": session_token,
"user_id": user_id, "user_id": user_id,
"expires_at": expires, "expires_at": expires,
"created_at": now, "created_at": now,
+1 -3
View File
@@ -74,7 +74,6 @@ async def test_delete_me(client, auth_headers):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_expired_session_rejected(client, db_engine): async def test_expired_session_rejected(client, db_engine):
"""Expired sessions must be rejected.""" """Expired sessions must be rejected."""
import hashlib
import secrets import secrets
import uuid import uuid
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
@@ -83,7 +82,6 @@ async def test_expired_session_rejected(client, db_engine):
user_id = str(uuid.uuid4()) user_id = str(uuid.uuid4())
session_token = secrets.token_urlsafe(32) session_token = secrets.token_urlsafe(32)
token_hash = hashlib.sha256(session_token.encode()).hexdigest()
now = datetime.now(UTC).isoformat() now = datetime.now(UTC).isoformat()
expired = (datetime.now(UTC) - timedelta(hours=1)).isoformat() expired = (datetime.now(UTC) - timedelta(hours=1)).isoformat()
@@ -110,7 +108,7 @@ async def test_expired_session_rejected(client, db_engine):
), ),
{ {
"id": str(uuid.uuid4()), "id": str(uuid.uuid4()),
"token": token_hash, "token": session_token,
"uid": user_id, "uid": user_id,
"ea": expired, "ea": expired,
"ca": now, "ca": now,