diff --git a/docs/configuration/password-hash.md b/docs/configuration/password-hash.md index df15f90e..dde47982 100644 --- a/docs/configuration/password-hash.md +++ b/docs/configuration/password-hash.md @@ -1,21 +1,24 @@ # Password hash -By default, FastAPI Users will use the [BCrypt algorithm](https://en.wikipedia.org/wiki/Bcrypt) to **hash and salt** passwords before storing them in the database. +By default, FastAPI Users will use the [Argon2 algorithm](https://en.wikipedia.org/wiki/Argon2) to **hash and salt** passwords before storing them in the database, with backwards-compatibility with [Bcrypt](https://en.wikipedia.org/wiki/Bcrypt). -The implementation is provided by [Passlib](https://passlib.readthedocs.io/en/stable/index.html), a battle-tested Python library for password hashing. +The implementation is provided by [pwdlib](https://github.com/frankie567/pwdlib), a modern password hashing wrapper. -## Customize `CryptContext` +## Customize `PasswordHash` -If you need to support other hashing algorithms, you can customize the [`CryptContext` object of Passlib](https://passlib.readthedocs.io/en/stable/lib/passlib.context.html#the-cryptcontext-class). +If you need to tune the algorithms used or their settings, you can customize the [`PasswordHash` object of pwdlib](https://frankie567.github.io/pwdlib/reference/pwdlib/#pwdlib.PasswordHash). -For this, you'll need to instantiate the `PasswordHelper` class and pass it your `CryptContext`. The example below shows you how you can create a `CryptContext` to add support for the Argon2 algorithm while deprecating BCrypt. +For this, you'll need to instantiate the `PasswordHelper` class and pass it your `PasswordHash`. The example below shows you how you can create a `PasswordHash` to only support the Argon2 algorithm. ```py from fastapi_users.password import PasswordHelper -from passlib.context import CryptContext +from pwdlib import PasswordHash, exceptions +from pwdlib.hashers.argon2 import Argon2Hasher -context = CryptContext(schemes=["argon2", "bcrypt"], deprecated="auto") -password_helper = PasswordHelper(context) +password_hash = PasswordHash(( + Argon2Hasher(), +)) +password_helper = PasswordHelper(password_hash) ``` Finally, pass the `password_helper` variable while instantiating your `UserManager`: @@ -32,12 +35,9 @@ async def get_user_manager(user_db=Depends(get_user_db)): If it is, we take the opportunity of having the password in plain-text at hand (since the user just logged in!) to hash it with a better algorithm and update it in database. -!!! warning "Dependencies for alternative algorithms are not included by default" - FastAPI Users won't install required dependencies to make other algorithms like Argon2 work. It's up to you to install them. - ## Full customization -If you don't wish to use Passlib at all – **which we don't recommend unless you're absolutely sure of what you're doing** — you can implement your own `PasswordHelper` class as long as it implements the `PasswordHelperProtocol` and its methods. +If you don't wish to use `pwdlib` at all – **which we don't recommend unless you're absolutely sure of what you're doing** — you can implement your own `PasswordHelper` class as long as it implements the `PasswordHelperProtocol` and its methods. ```py from typing import Tuple diff --git a/fastapi_users/password.py b/fastapi_users/password.py index d7bfa783..4dbb4c95 100644 --- a/fastapi_users/password.py +++ b/fastapi_users/password.py @@ -1,13 +1,15 @@ -from typing import Optional, Protocol, Tuple +import secrets +from typing import Optional, Protocol, Tuple, Union -from passlib import pwd -from passlib.context import CryptContext +from pwdlib import PasswordHash +from pwdlib.hashers.argon2 import Argon2Hasher +from pwdlib.hashers.bcrypt import BcryptHasher class PasswordHelperProtocol(Protocol): def verify_and_update( self, plain_password: str, hashed_password: str - ) -> Tuple[bool, str]: ... # pragma: no cover + ) -> Tuple[bool, Union[str, None]]: ... # pragma: no cover def hash(self, password: str) -> str: ... # pragma: no cover @@ -15,19 +17,24 @@ class PasswordHelperProtocol(Protocol): class PasswordHelper(PasswordHelperProtocol): - def __init__(self, context: Optional[CryptContext] = None) -> None: - if context is None: - self.context = CryptContext(schemes=["bcrypt"], deprecated="auto") + def __init__(self, password_hash: Optional[PasswordHash] = None) -> None: + if password_hash is None: + self.password_hash = PasswordHash( + ( + Argon2Hasher(), + BcryptHasher(), + ) + ) else: - self.context = context # pragma: no cover + self.password_hash = password_hash # pragma: no cover def verify_and_update( self, plain_password: str, hashed_password: str - ) -> Tuple[bool, str]: - return self.context.verify_and_update(plain_password, hashed_password) + ) -> Tuple[bool, Union[str, None]]: + return self.password_hash.verify_and_update(plain_password, hashed_password) def hash(self, password: str) -> str: - return self.context.hash(password) + return self.password_hash.hash(password) def generate(self) -> str: - return pwd.genword() + return secrets.token_urlsafe() diff --git a/pyproject.toml b/pyproject.toml index 841c2a31..fec0d440 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -141,8 +141,7 @@ classifiers = [ requires-python = ">=3.8" dependencies = [ "fastapi >=0.65.2", - "passlib[bcrypt] ==1.7.4; python_version < '3.12'", - "bcrypt ==4.1.2; python_version >= '3.12'", + "pwdlib[argon2,bcrypt] ==0.2.0", "email-validator >=1.1.0,<2.2", "pyjwt[crypto] ==2.8.0", "python-multipart ==0.0.7",