Replace passlib in favor of pwdlib

This commit is contained in:
François Voron
2024-03-11 14:04:50 +01:00
parent e7972561c0
commit f7a31c579d
3 changed files with 32 additions and 26 deletions

View File

@@ -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

View File

@@ -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()

View File

@@ -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",