fix(auth): use real bcrypt hash for anti-enumeration dummy verify
The hardcoded placeholder '$2b$12$abc...' wasn't a valid bcrypt
hash (checksum had wrong base64 length), so passlib raised
ValueError on every user-not-found login attempt:
ValueError: malformed bcrypt hash (checksum must be ...)
Replace with hash_password('dummy-not-a-real-password') computed
once at module import. Same constant-time intent (verify cost is
identical to a real password check) but actually valid input.
This commit is contained in:
@@ -13,7 +13,7 @@ from datetime import datetime, timedelta
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.auth.jwt_handler import verify_password
|
||||
from app.auth.jwt_handler import hash_password, verify_password
|
||||
from app.auth.strategies.base import AuthError, AuthStrategy, ExternalIdentity
|
||||
from app.models.user import AuthProvider, User
|
||||
|
||||
@@ -22,6 +22,12 @@ logger = logging.getLogger(__name__)
|
||||
ACCOUNT_LOCKOUT_THRESHOLD = 5
|
||||
ACCOUNT_LOCKOUT_DURATION_MIN = 15
|
||||
|
||||
# Pre-computed valid bcrypt hash used in the user-not-found branch so that
|
||||
# verify cost is constant-time regardless of whether the username exists.
|
||||
# Generated once at module import. The plaintext is irrelevant — it is never
|
||||
# the user's secret and cannot match any real password.
|
||||
_DUMMY_HASH = hash_password("dummy-not-a-real-password")
|
||||
|
||||
|
||||
class LocalAuthStrategy(AuthStrategy):
|
||||
"""
|
||||
@@ -45,7 +51,7 @@ class LocalAuthStrategy(AuthStrategy):
|
||||
|
||||
if user is None or user.auth_provider != AuthProvider.LOCAL:
|
||||
# Constant-time dummy verify to prevent timing-based enumeration.
|
||||
verify_password("dummy", "$2b$12$abcdefghijklmnopqrstuvwxyz0123456789012345678901AB")
|
||||
verify_password("anything", _DUMMY_HASH)
|
||||
raise AuthError(
|
||||
detail=f"unknown user '{username}'"
|
||||
if user is None
|
||||
|
||||
Reference in New Issue
Block a user