fix(rmi-backend): drop passlib — route backup codes through direct bcrypt
Some checks failed
CI / build (pull_request) Failing after 2s
Some checks failed
CI / build (pull_request) Failing after 2s
The reported symptom (AttributeError: module 'bcrypt' has no attribute
'__about__') was misleading. passlib 1.7.4 (the latest released version
— the project is unmaintained) actually traps that AttributeError with
a bare `except:` and continues. The real failure occurs later in
`passlib.handlers.bcrypt._finalize_backend_mixin`:
detect_wrap_bug -> verify(secret, bug_hash)
where secret = (b"0123456789"*26)[:255] # 255-byte test secret
bcrypt 4.0+ removed silent 72-byte truncation and now raises
`ValueError: password cannot be longer than 72 bytes`, breaking the
backend probe and crashing every `pwd_context.hash()/verify()` call.
Three options were considered:
A. Upgrade passlib — impossible, 1.7.4 is the latest PyPI release.
B. Pin bcrypt<4.0 — conflicts with chromadb 1.1.1 (>=4.0.1 required),
even though chromadb's bcrypt usage is optional and still works
at runtime in practice.
C. Shim bcrypt.__about__ — doesn't fix the actual truncation error.
Chosen: refactor app/auth.py to use bcrypt directly for backup-code
hashing too (the main `hash_password`/`verify_password` already did).
Drops the passlib dependency entirely, restoring bcrypt 5.x for chromadb
compatibility and eliminating the unmaintained passlib pin.
Verified:
* hash_password / verify_password round-trip OK
* _hash_backup_code / _verify_backup_code round-trip OK on
freshly generated 8-digit backup codes
* `pytest tests/unit` -> 717 passed, 32 warnings, 60 subtests passed
This commit is contained in:
parent
6b29227131
commit
1171a35d9f
3 changed files with 4 additions and 9 deletions
10
app/auth.py
10
app/auth.py
|
|
@ -12,9 +12,9 @@ import secrets
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import bcrypt
|
||||||
from fastapi import APIRouter, HTTPException, Request
|
from fastapi import APIRouter, HTTPException, Request
|
||||||
from jose import JWTError, jwt
|
from jose import JWTError, jwt
|
||||||
from passlib.context import CryptContext
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from app.core.redis import get_redis
|
from app.core.redis import get_redis
|
||||||
|
|
@ -86,12 +86,12 @@ def _generate_backup_codes(count: int = 8) -> list:
|
||||||
|
|
||||||
def _hash_backup_code(code: str) -> str:
|
def _hash_backup_code(code: str) -> str:
|
||||||
"""Hash a backup code for storage (same as password hashing)."""
|
"""Hash a backup code for storage (same as password hashing)."""
|
||||||
return pwd_context.hash(code)
|
return hash_password(code)
|
||||||
|
|
||||||
|
|
||||||
def _verify_backup_code(code: str, hashed: str) -> bool:
|
def _verify_backup_code(code: str, hashed: str) -> bool:
|
||||||
"""Verify a backup code against its hash."""
|
"""Verify a backup code against its hash."""
|
||||||
return pwd_context.verify(code, hashed)
|
return verify_password(code, hashed)
|
||||||
|
|
||||||
|
|
||||||
# ── Config ──
|
# ── Config ──
|
||||||
|
|
@ -99,10 +99,6 @@ JWT_SECRET = os.getenv("JWT_SECRET", "rmi-jwt-secret-change-me")
|
||||||
JWT_ALGORITHM = "HS256"
|
JWT_ALGORITHM = "HS256"
|
||||||
JWT_EXPIRY_DAYS = 7
|
JWT_EXPIRY_DAYS = 7
|
||||||
|
|
||||||
import bcrypt # noqa: E402
|
|
||||||
|
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
||||||
|
|
||||||
|
|
||||||
def hash_password(password: str) -> str:
|
def hash_password(password: str) -> str:
|
||||||
"""Hash password using bcrypt directly."""
|
"""Hash password using bcrypt directly."""
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ from app.services.supabase_service import ( # noqa: E402
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Lazy auth dependency - avoids importing passlib+jose+bcrypt at module load
|
# Lazy auth dependency - avoids importing jose+bcrypt at module load
|
||||||
async def _require_public_profile(request: Request):
|
async def _require_public_profile(request: Request):
|
||||||
from app.auth import require_public_profile
|
from app.auth import require_public_profile
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ pydantic>=2.9.0
|
||||||
python-telegram-bot>=21.0
|
python-telegram-bot>=21.0
|
||||||
supabase>=2.0.0
|
supabase>=2.0.0
|
||||||
python-jose[cryptography]>=3.3.0
|
python-jose[cryptography]>=3.3.0
|
||||||
passlib[bcrypt]>=1.7.4
|
|
||||||
python-multipart>=0.0.17
|
python-multipart>=0.0.17
|
||||||
aiohttp>=3.11.0
|
aiohttp>=3.11.0
|
||||||
feedparser>=6.0.11
|
feedparser>=6.0.11
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue