feat(auth): switch to bcrypt primary scheme, keep sha256_crypt for migration

- database.py: CryptContext = ['bcrypt', 'sha256_crypt'] with sha256_crypt deprecated
- database.py: add verify_and_update_password() returning (ok, new_hash)
- main.py: login uses verify_and_update; on successful verify transparently
  commits new bcrypt hash when old hash is sha256_crypt
- requirements.txt: passlib[bcrypt]==1.7.4 + bcrypt==4.0.1 (compat with passlib 1.7.4)

Migration: zero-touch. Admin's first login after this deploy will
auto-upgrade the stored hash from $5$... to $2b$...

Avoids the passlib sha256_crypt cross-version verify bug that bit us
this morning. bcrypt is OpenSSL-native and stable across passlib
versions.
This commit is contained in:
IT狗
2026-07-22 12:07:51 +08:00
parent f6712d9d0e
commit acbc767247
3 changed files with 29 additions and 4 deletions
+14 -1
View File
@@ -15,7 +15,11 @@ engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
pwd_context = CryptContext(schemes=["sha256_crypt"], deprecated="auto")
# bcrypt is the primary scheme (cross-version stable, OpenSSL-native).
# sha256_crypt is kept as deprecated fallback so legacy hashes still
# verify; on first successful login the password is re-hashed to bcrypt
# via verify_and_update().
pwd_context = CryptContext(schemes=["bcrypt", "sha256_crypt"], deprecated=["sha256_crypt"])
# JWT Settings
SECRET_KEY = secret.JWT_SECRET if hasattr(secret, 'JWT_SECRET') else "aars-dev-secret-change-in-production"
@@ -34,6 +38,15 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def verify_and_update_password(plain_password: str, hashed_password: str) -> tuple[bool, str | None]:
"""Verify password and return new hash if rehash is needed.
Returns (ok, new_hash_or_none). Caller is responsible for committing
the new hash to the DB when new_hash is not None.
"""
return pwd_context.verify_and_update(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)