Security hardening.
- Login now returns the same generic HTTP 401 for both invalid credentials and disabled accounts, removing an account-status enumeration oracle (CWE-204).
- Added an optional
rate_limiterhook oninit_users/attach_routes, awaited on the login and password-reset-request endpoints to enable brute-force / email-bombing protection (CWE-307). - Verification / reset email helpers raise a clear error if the URL template still points at the placeholder
example.comdomain.
Initial release.
SQLAlchemyBaseUserTabledeclarative mixin — UUID4 id, normalized email, argon2id hash,is_active/is_verified/is_superuser,password_version.UserManager—create/get_by_email/authenticate/request_verify/verify/request_password_reset/reset_password/update/delete.UserTokens—email_verify+password_resetJWTs with type binding +password_versioninvalidation.- Default Jinja2 templates for verification + reset emails (HTML + plain text).
- HTTP routes under
/users—register,login,verify/request,verify/{token},password-reset/request,password-reset/{token}. - Account enumeration prevention —
/verify/requestand/password-reset/requestalways 202. - Timing-equalized authentication — argon2id runs against a dummy hash on unknown emails.
- Reset bumps
password_versionto invalidate every outstanding token in one DB write. - Minimum 32-character token signing secret enforced at construction time.
init_users(app, manager=...)+Depends(get_user_manager)+WeakKeyDictionaryregistry.
Security review applied before ship:
- Fixed double-decode bug (CWE-287) —
verify()andreset_password()now usepeek_*_token_sub()to extract the user_id before the DB lookup, then run the fullpvcheck against the loaded user. Without this fix, any user whosepassword_versionhad ever been bumped (anyone who completed a password reset) would be unable to verify or reset again. - Documented three known accepted risks in README: 409-on-register enumeration, 403-on-inactive credential confirmation, absence of built-in rate limiting / session issuing.