From 31ee641090d070b792978e479f265ceecccb9141 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brzuszek=20Ma=C3=ABl?= Date: Wed, 18 Oct 2023 10:58:17 +0200 Subject: [PATCH 1/3] Use bcrypt instead of passlib --- app/core/security.py | 33 +++++++++++++++++++-------------- requirements.txt | 2 +- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/app/core/security.py b/app/core/security.py index e521129cc..a224b29fe 100644 --- a/app/core/security.py +++ b/app/core/security.py @@ -1,9 +1,9 @@ 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 @@ -11,7 +11,6 @@ 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. @@ -36,12 +35,23 @@ """ +def generate_token(nbytes=32) -> str: + """ + Generate a `nbytes` bytes cryptographically strong random urlsafe token using the *secrets* library. + + By default, a 32 bytes token is generated. + """ + # 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 get_password_hash(password: str) -> 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. """ - return pwd_context.hash(password) + hashed = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt(rounds=13)) + return hashed.decode("utf-8") def verify_password(plain_password: str, hashed_password: str | None) -> bool: @@ -50,17 +60,12 @@ def verify_password(plain_password: str, hashed_password: str | None) -> bool: Pass hashed_password=None to simulate the delay a real verification would have taken. This is useful to limit timing attacks """ - return pwd_context.verify(plain_password, hashed_password) - - -def generate_token(nbytes=32) -> str: - """ - Generate a `nbytes` bytes cryptographically strong random urlsafe token using the *secrets* library. - - By default, a 32 bytes token is generated. - """ - # 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( diff --git a/requirements.txt b/requirements.txt index e8c2be2b1..0da1c3f3d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 From 1b1cff98dea03f01afacb864853458dfadb857dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brzuszek=20Ma=C3=ABl?= Date: Wed, 18 Oct 2023 11:07:12 +0200 Subject: [PATCH 2/3] Update comments --- app/core/security.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/core/security.py b/app/core/security.py index a224b29fe..a385b21d6 100644 --- a/app/core/security.py +++ b/app/core/security.py @@ -47,7 +47,7 @@ def generate_token(nbytes=32) -> str: def get_password_hash(password: str) -> str: """ - Return a salted hash computed from password. The function use a bcrypt based *passlib* CryptContext. + Return a salted hash computed from password. Both the salt and the algorithm identifier are included in the hash. """ hashed = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt(rounds=13)) @@ -56,9 +56,10 @@ def get_password_hash(password: str) -> str: def verify_password(plain_password: str, hashed_password: str | None) -> bool: """ - Compare `plain_password` against its salted hash representation `hashed_password`. The function use a bcrypt based *passlib* CryptContext. + Compare `plain_password` against its salted hash representation `hashed_password`. - Pass hashed_password=None to simulate the delay a real verification would have taken. This is useful to limit timing attacks + 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. """ fake_hash = bcrypt.hashpw(generate_token(12).encode("utf-8"), bcrypt.gensalt(13)) if hashed_password is None: From 31021d7be97e505b72da3a56cf861623c3b23135 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brzuszek=20Ma=C3=ABl?= Date: Wed, 18 Oct 2023 11:14:53 +0200 Subject: [PATCH 3/3] Update comment --- app/core/security.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/core/security.py b/app/core/security.py index a385b21d6..9d82283a1 100644 --- a/app/core/security.py +++ b/app/core/security.py @@ -12,10 +12,9 @@ from app.schemas import schemas_auth """ -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. """