Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

password requirements #377

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
12 changes: 12 additions & 0 deletions app/app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""File defining the Metadata. And the basic functions creating the database tables and calling the router"""

import json
import logging
import uuid
from collections.abc import AsyncGenerator, Awaitable, Callable
Expand All @@ -20,6 +21,7 @@
from sqlalchemy.engine import Connection, Engine
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from zxcvbn.matching import add_frequency_lists

from app import api
from app.core import models_core
Expand Down Expand Up @@ -304,6 +306,16 @@ async def lifespan(app: FastAPI) -> AsyncGenerator:
):
hyperion_error_logger.info("Redis client not configured")

# We add custom dictionnaries for password verification with zxcvb.
# Dictionnaries come from https://github.com/zxcvbn-ts/zxcvbn/tree/master/packages/languages
password_dicts: dict[str, list[str]] = {}
for password_dict_file in Path("assets/password_dict").glob("*/*"):
with Path.open(password_dict_file) as file:
password_dicts[
f"{password_dict_file.parts[-2]}_{password_dict_file.stem}"
] = json.load(file)
add_frequency_lists(password_dicts)

@app.middleware("http")
async def logging_middleware(
request: Request,
Expand Down
23 changes: 19 additions & 4 deletions app/utils/validators.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import re

import zxcvbn

"""
A collection of Pydantic validators
See https://pydantic-docs.helpmanual.io/usage/validators/#reuse-validators
Expand All @@ -10,10 +14,21 @@
This function is intended to be used as a Pydantic validator:
https://pydantic-docs.helpmanual.io/usage/validators/#reuse-validators
"""
# TODO
if len(password) < 6:
raise ValueError("The password must be at least 6 characters long")
return password.strip()
password = password.strip()
if not re.fullmatch(
r"(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$€%^&*\(\)_+\-.,.?\":\{\}|<>'/;\[\]]).*",
password,
):
raise ValueError(

Check warning on line 22 in app/utils/validators.py

View check run for this annotation

Codecov / codecov/patch

app/utils/validators.py#L22

Added line #L22 was not covered by tests
"The password must contain at least one number, one special character, one majuscule and one minuscule.",
)
result = zxcvbn.zxcvbn(password)
if not result["score"] == 4:
raise ValueError(

Check warning on line 27 in app/utils/validators.py

View check run for this annotation

Codecov / codecov/patch

app/utils/validators.py#L27

Added line #L27 was not covered by tests
result["feedback"],
)

return password


def email_normalizer(email: str) -> str:
Expand Down
1 change: 1 addition & 0 deletions assets/password_dict/fr/commonWords.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions assets/password_dict/fr/firstnames.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions assets/password_dict/fr/lastnames.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions assets/password_dict/fr/wikipedia.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion requirements-common.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ redis==5.0.7
requests==2.32.3
SQLAlchemy[asyncio]==2.0.31 # [asyncio] allows greenlet to be installed on Apple M1 devices.
unidecode==1.3.8
uvicorn[standard]==0.29.0
uvicorn[standard]==0.29.0
zxcvbn==4.4.28
3 changes: 2 additions & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ pytest==8.2.2
ruff==0.4.8
types-aiofiles==24.1.0.20240626
types-redis==4.6.0.20240425
types-requests==2.32.0.20240622
types-requests==2.32.0.20240622
types-zxcvbn==4.4.1.20240106
8 changes: 4 additions & 4 deletions tests/test_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
FABRISTPP_EMAIL_2 = "fabristpp.eclair3@ecl21.ec-lyon.fr"

student_user_email = "student@etu.ec-lyon.fr"
student_user_password = "password"
student_user_password = "Password1!"


@pytest_asyncio.fixture(scope="module", autouse=True)
Expand Down Expand Up @@ -162,7 +162,7 @@ def test_create_and_activate_user(mocker: MockerFixture, client: TestClient) ->
"/users/activate",
json={
"activation_token": UNIQUE_TOKEN,
"password": "password",
"password": "eclair!AEECL69",
"firstname": "firstname",
"name": "name",
"nickname": "nickname",
Expand Down Expand Up @@ -212,7 +212,7 @@ def test_recover_and_reset_password(mocker: MockerFixture, client: TestClient) -

response = client.post(
"/users/reset-password",
json={"reset_token": UNIQUE_TOKEN, "new_password": "new_password"},
json={"reset_token": UNIQUE_TOKEN, "new_password": "eclar!AEECL69"},
)

assert response.status_code == 201
Expand Down Expand Up @@ -302,7 +302,7 @@ def test_change_password(client: TestClient) -> None:
json={
"email": student_user_email,
"old_password": student_user_password,
"new_password": "the_new_password",
"new_password": "pluseclair!AEECL69",
},
headers={"Authorization": f"Bearer {token_student_user}"},
)
Expand Down