From ca40d251d622d737b757b79f72fcb6c228c92afb Mon Sep 17 00:00:00 2001 From: Vadym Samoilenko Date: Thu, 26 Mar 2026 13:10:24 +0000 Subject: [PATCH] fix: replace passlib with bcrypt directly (passlib incompatible with bcrypt>=4) Co-Authored-By: Claude Sonnet 4.6 --- requirements.txt | 2 +- src/auth.py | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index a05e866..0337228 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,6 @@ alembic==1.14.0 pydantic[email]==2.10.3 pydantic-settings==2.6.1 python-jose[cryptography]==3.3.0 -passlib[bcrypt]==1.7.4 +bcrypt==4.2.1 python-multipart==0.0.20 httpx==0.28.1 diff --git a/src/auth.py b/src/auth.py index c6d50c2..ec21fc9 100644 --- a/src/auth.py +++ b/src/auth.py @@ -2,10 +2,10 @@ import secrets from datetime import datetime, timedelta, timezone from typing import Annotated +import bcrypt from fastapi import Depends, HTTPException, Security, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from jose import JWTError, jwt -from passlib.context import CryptContext from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession @@ -13,7 +13,6 @@ from src.config import settings from src.database import get_db from src.models import ApiKey, User -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") bearer_scheme = HTTPBearer(auto_error=False) ALGORITHM = "HS256" @@ -22,11 +21,11 @@ ALGORITHM = "HS256" # ── Password ────────────────────────────────────────────────────────────────── def hash_password(password: str) -> str: - return pwd_context.hash(password) + return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() def verify_password(plain: str, hashed: str) -> bool: - return pwd_context.verify(plain, hashed) + return bcrypt.checkpw(plain.encode(), hashed.encode()) # ── JWT ─────────────────────────────────────────────────────────────────────── @@ -60,7 +59,7 @@ def generate_api_key() -> tuple[str, str, str]: """Returns (raw_key, prefix, hash). raw_key shown once to user.""" raw = "cc_" + secrets.token_urlsafe(32) prefix = raw[:11] # "cc_" + 8 chars - return raw, prefix, pwd_context.hash(raw) + return raw, prefix, hash_password(raw) async def verify_api_key(raw_key: str, db: AsyncSession) -> User | None: @@ -75,7 +74,7 @@ async def verify_api_key(raw_key: str, db: AsyncSession) -> User | None: ) keys = result.scalars().all() for key in keys: - if pwd_context.verify(raw_key, key.key_hash): + if verify_password(raw_key, key.key_hash): key.last_used_at = datetime.now(timezone.utc) await db.commit() return key.user