Skip to content

Commit

Permalink
Use bcrypt instead of passlib (#270)
Browse files Browse the repository at this point in the history
### Description

Use the bcrypt library for password hashing instead of passlib that used
a deprecated dependency (crypt).

### Checklist

- [x] Created tests which fail without the change (if possible)
- [x] All tests passing

---------

Co-authored-by: Brzuszek Maël <[email protected]>
  • Loading branch information
Jake-Ransom and Brzuszek Maël authored Oct 19, 2023
1 parent 81bdd1b commit 9adfbeb
Show file tree
Hide file tree
Showing 2 changed files with 25 additions and 20 deletions.
43 changes: 24 additions & 19 deletions app/core/security.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import secrets
from datetime import datetime, timedelta

import bcrypt
from fastapi.security import OAuth2AuthorizationCodeBearer
from jose import jwt
from passlib.context import CryptContext
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.config import Settings
from app.cruds import cruds_users
from app.models import models_core
from app.schemas import schemas_auth

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto", bcrypt__rounds=13)
"""
In order to salt and hash password, we use a *passlib* [CryptContext](https://passlib.readthedocs.io/en/stable/narr/quickstart.html) object.
In order to salt and hash password, we the bcrypt hashing function (see https://en.wikipedia.org/wiki/Bcrypt).
We use "bcrypt" to hash password, a different hash will be added automatically for each password. See [Auth0 Understanding bcrypt](https://auth0.com/blog/hashing-in-action-understanding-bcrypt/) for information about bcrypt.
deprecated="auto" may be used to do password hash migration, see [Passlib hash migration](https://passlib.readthedocs.io/en/stable/narr/context-tutorial.html#deprecation-hash-migration).
A different salt will be added automatically for each password. See [Auth0 Understanding bcrypt](https://auth0.com/blog/hashing-in-action-understanding-bcrypt/) for information about bcrypt.
It is important to use enough rounds while accounting for the hash computation time. Default is 12. 13 allows for a 0.5 seconds computing delay.
"""

Expand All @@ -36,31 +34,38 @@
"""


def get_password_hash(password: str) -> str:
def generate_token(nbytes=32) -> str:
"""
Return a salted hash computed from password. The function use a bcrypt based *passlib* CryptContext.
Both the salt and the algorithm identifier are included in the hash.
Generate a `nbytes` bytes cryptographically strong random urlsafe token using the *secrets* library.
By default, a 32 bytes token is generated.
"""
return pwd_context.hash(password)
# We use https://docs.python.org/3/library/secrets.html#secrets.token_urlsafe to generate the activation secret token
return secrets.token_urlsafe(nbytes)


def verify_password(plain_password: str, hashed_password: str | None) -> bool:
def get_password_hash(password: str) -> str:
"""
Compare `plain_password` against its salted hash representation `hashed_password`. The function use a bcrypt based *passlib* CryptContext.
Pass hashed_password=None to simulate the delay a real verification would have taken. This is useful to limit timing attacks
Return a salted hash computed from password.
Both the salt and the algorithm identifier are included in the hash.
"""
return pwd_context.verify(plain_password, hashed_password)
hashed = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt(rounds=13))
return hashed.decode("utf-8")


def generate_token(nbytes=32) -> str:
def verify_password(plain_password: str, hashed_password: str | None) -> bool:
"""
Generate a `nbytes` bytes cryptographically strong random urlsafe token using the *secrets* library.
Compare `plain_password` against its salted hash representation `hashed_password`.
By default, a 32 bytes token is generated.
We genrerate a fake_hash for the case where hashed_password=None (ie the email isn't valid) to simulate the delay a real verification would have taken.
This is useful to limit timing attacks that could be used to guess valid emails.
"""
# We use https://docs.python.org/3/library/secrets.html#secrets.token_urlsafe to generate the activation secret token
return secrets.token_urlsafe(nbytes)
fake_hash = bcrypt.hashpw(generate_token(12).encode("utf-8"), bcrypt.gensalt(13))
if hashed_password is None:
return bcrypt.checkpw(plain_password.encode("utf-8"), fake_hash)
return bcrypt.checkpw(
plain_password.encode("utf-8"), hashed_password.encode("utf-8")
)


async def authenticate_user(
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
alembic==1.11.3 # database migrations
fastapi==0.99.0
Jinja2==3.1.2 # template engine for html files
passlib[bcrypt]==1.7.4 # password hashing using bcrypt
bcrypt==4.0.1 # password hashing
python-dotenv==1.0.0 # load environment variables from .env file
python-jose[cryptography]==3.3.0 # generate and verify the JWT tokens
python-multipart==0.0.6 # a form data parser, as oauth flow requires form-data parameters
Expand Down

0 comments on commit 9adfbeb

Please sign in to comment.