fix(api): hash session token with SHA-256 before DB lookup

Better-Auth v1.2+ stores SHA-256(raw_token) in the sessions.token
column. The cookie/Bearer header carries the raw token, so the API was
doing a plain-text lookup that would never match a hashed value —
causing all authenticated endpoints to return 401.

- Add hashlib import and hash token in _validate_session_token()
- Update conftest._create_test_user_and_session() to store hashed tokens
- Update test_expired_session_rejected() to store hashed tokens

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Barcode Betty
2026-04-04 19:00:09 +00:00
parent ffeae96d17
commit 89293d1811
3 changed files with 14 additions and 5 deletions
+6 -2
View File
@@ -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()
+5 -2
View File
@@ -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,
+3 -1
View File
@@ -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,