fix: replace passlib with direct bcrypt — eliminate `(trapped)` warning (#853)
Drops the passlib dependency entirely. passlib has been unmaintained since August 2020 (last release 1.7.4) and doesn't know about bcrypt 4.1+ — it tries to read `bcrypt.__about__.__version__`, gets an AttributeError, traps it internally, and prints this on every login: (trapped) error reading bcrypt version Traceback (most recent call last): File "/opt/venv/lib/python3.11/site-packages/passlib/handlers/bcrypt.py", line 620, in _load_backend_mixin version = _bcrypt.__about__.__version__ ^^^^^^^^^^^^^^^^^ AttributeError: module 'bcrypt' has no attribute '__about__' The warning is cosmetic (passlib falls back to default behavior, auth keeps working — the hotfix in #851 was the actual blocker), but it's log noise on every login. Going forward, every bcrypt release widens the gap and the chance of a behavioral break grows. Fix: replace `passlib.context.CryptContext` with direct bcrypt calls in the 4 places that used it. The hash format is identical (`$2b$NN$...` — passlib's bcrypt scheme writes the same format raw bcrypt does), so existing DB hashes for users and TOTP backup codes continue to verify. Touched: - app/auth/utils.py: AuthHandler.get_password_hash + verify_password now use bcrypt.hashpw / bcrypt.checkpw directly. Drops the pwd_context class attribute. - app/auth/services/totp.py: backup code hash + verify also direct bcrypt. Drops the _pwd_ctx module-level CryptContext. - app/auth/services/sso.py: SSO auto-provisioning random-password generation + hash now direct bcrypt. Reduced secrets.token_urlsafe(64) → token_urlsafe(48) (~64 chars) so the generated random_pw stays under bcrypt's 72-byte limit explicitly rather than relying on silent truncation. - requirements.in: dropped `passlib` and `passlib[bcrypt]`. Updated the bcrypt<5 cap comment (the original reason — passlib incompatibility — is gone; the cap now exists only because bcrypt 5 raises on >72-byte passwords instead of silently truncating, which is a separate concern). Verified locally: - login HTTP 200 - no `(trapped)` warning, no `passlib` reference anywhere in logs - 2FA setup returns 8 backup codes; the new bcrypt-direct path generates the same `$2b$` hash format - format-compatibility check: bcrypt.hashpw produces the exact same `$2b$NN$...` envelope passlib's bcrypt scheme produced, so all pre-existing user passwords and backup codes in production DBs will continue to verify without any migration Follow-up (out of scope here): with passlib gone, the bcrypt<5 cap exists only to dodge bcrypt 5's strict-error-on->72-byte behavior. A separate PR can lift it after enforcing a 72-byte (or pre-hash- with-SHA-256) limit at the model layer for password fields like `max_length=256` in PasswordReset. Co-authored-by: taylor_socfortress <taylor.walton@socfortress.co> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>