Compare commits

..

4 Commits

Author SHA1 Message Date
Barcode Betty 25edd8d5e3 fix(api): revert SHA-256 session token hashing — better-auth stores raw tokens
Better-auth v1.5.6 stores raw 32-char tokens in sessions.token, not SHA-256
hashes. The SHA-256 fix from PR #136 causes all authenticated API calls to
return 401 because the UAT sessions table contains raw tokens.

- Remove hashlib from dependencies.py; compare tokens directly
- Remove hashlib from conftest.py; store raw tokens in test DB
- Remove hashlib from test_expired_session_rejected; use raw tokens

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 19:21:26 +00:00
cartsnitch-cto[bot] bd3cb3b9ab fix(api): hash session token with SHA-256 before DB lookup (#136)
fix(api): hash session token with SHA-256 before DB lookup
2026-04-04 19:06:30 +00:00
cartsnitch-cto[bot] 3bedc651c6 Merge pull request #133 from cartsnitch/fix/alembic-version-table-width
fix(api): widen alembic version_table column to 128 chars
2026-04-04 19:01:09 +00:00
Pawla Abdul 43ee1c3531 fix(api): widen alembic version_table column to 128 chars
Default varchar(32) alembic_version column truncates long revision IDs
like 003_make_users_hashed_password_nullable (39 chars) on fresh databases.
Set version_table_column_width=128 in both context.configure() calls.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-04 18:32:36 +00:00
4 changed files with 9 additions and 15 deletions
+2 -1
View File
@@ -31,6 +31,7 @@ def run_migrations_offline() -> None:
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
version_table_column_width=128,
)
with context.begin_transaction():
context.run_migrations()
@@ -44,7 +45,7 @@ def run_migrations_online() -> None:
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
context.configure(connection=connection, target_metadata=target_metadata, version_table_column_width=128)
with context.begin_transaction():
context.run_migrations()
# Create any tables defined in models but not yet created by migrations.
+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.
"""
import hashlib
from datetime import UTC, datetime
from fastapi import Cookie, Depends, Header, HTTPException, Request, status
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:
"""Validate a Better-Auth session token against the sessions table.
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.
Better-Auth stores the raw token in the DB. The cookie/Bearer header
carries the same raw token, so we compare directly.
"""
token_hash = hashlib.sha256(token.encode()).hexdigest()
result = await db.execute(
text("SELECT user_id, expires_at FROM sessions WHERE token = :token"),
{"token": token_hash},
{"token": token},
)
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.
"""
import hashlib
import secrets
import uuid
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]:
"""Create a test user and a valid session directly in the DB.
Returns (user_dict, session_token). Better-Auth v1.2+ stores SHA-256
hashed tokens in the DB, so the token is hashed before insertion.
Returns (user_dict, session_token). Better-Auth stores the raw token
in the DB, so we insert it as-is.
"""
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()
@@ -172,7 +170,7 @@ async def _create_test_user_and_session(client: AsyncClient, db_engine, **user_o
),
{
"id": session_id,
"token": token_hash,
"token": session_token,
"user_id": user_id,
"expires_at": expires,
"created_at": now,
+1 -3
View File
@@ -74,7 +74,6 @@ 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
@@ -83,7 +82,6 @@ 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()
@@ -110,7 +108,7 @@ async def test_expired_session_rejected(client, db_engine):
),
{
"id": str(uuid.uuid4()),
"token": token_hash,
"token": session_token,
"uid": user_id,
"ea": expired,
"ca": now,