Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ test:
source ./venv/bin/activate && python3 -m pytest --verbosity=2 --showlocals --log-level=DEBUG

create-user:
python -m auth_backend user create --email test-user@profcomff.com --password string
python -m auth_backend user create --email test-user@profcomff.com --password string12

create-admin:
source ./venv/bin/activate && python -m auth_backend user create --email test-admin@profcomff.com --password string
source ./venv/bin/activate && python -m auth_backend user create --email test-admin@profcomff.com --password string12
source ./venv/bin/activate && python -m auth_backend scope create --name auth.group.create --comment auth.group.create --creator_email test-admin@profcomff.com
source ./venv/bin/activate && python -m auth_backend scope create --name auth.group.delete --comment auth.group.delete --creator_email test-admin@profcomff.com
source ./venv/bin/activate && python -m auth_backend scope create --name auth.group.read --comment auth.group.read --creator_email test-admin@profcomff.com
Expand All @@ -61,7 +61,7 @@ create-admin:
source ./venv/bin/activate && python -m auth_backend user_group create --email test-admin@profcomff.com

login-user:
curl -X 'POST' 'http://localhost:8000/email/login' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"email": "test-user@profcomff.com", "password": "string"}'
curl -X 'POST' 'http://localhost:8000/email/login' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"email": "test-user@profcomff.com", "password": "string12"}'

login-admin:
curl -X 'POST' 'http://localhost:8000/email/login' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"email": "test-admin@profcomff.com", "password": "string"}'
curl -X 'POST' 'http://localhost:8000/email/login' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"email": "test-admin@profcomff.com", "password": "string12"}'
20 changes: 14 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,21 @@ foo@bar:~$ python -m auth_backend start

## Сценарий использования
### Email: регистрация нового аккаунта
1. Дернуть ручку `POST /email/registrate` . Вы передаете
1. Дернуть ручку `POST /email/registration`. Вы передаете
```json
{
"email": "string", // Почта
"password": "string" // Пароль
"email": "user@example.com",
"password": "Password1!"
}
```

Требования к новому паролю:
- длина от 8 до 32 символов;
- разрешены латинские буквы `A-Z`, `a-z`, цифры `0-9` и стандартные ASCII-спецсимволы;
- пробелы, управляющие символы и символы вне ASCII (например, кириллица) запрещены.

Эти же требования применяются при смене и восстановлении пароля. При нарушении требований API возвращает `422 Unprocessable Entity`.

3. На почту приходит письмо с линком на `GET /email/approve?token='...'`, если по ней перейти то почта будет подтверждена и регистрацию можно считать завершенной.

### Email: вход в аккаунт
Expand All @@ -118,16 +126,16 @@ foo@bar:~$ python -m auth_backend start
3. Вам придет письмо, где будет ссылка НА ФРОНТ(надо сделать это), в ссылке будет reset_token
4. Токен надо передать в ручку `POST /email/reset/password` в заголовках, вместе с
```json
{"new_password": ""}
{"new_password": "NewPassword1!"}
```
и пароль будет изменен

### Email: Изменение пароля
1. Если пароль не забыт, а просто надо его поменять. Тогда в `POST /email/reset/password/request` передается токен авторизации, в теле вы передаете
```json
{
"password": "string", // старый пароль
"new_password": "string" // новый пароль
"password": "CurrentPassword1!", // старый пароль
"new_password": "NewPassword1!" // новый пароль
}
```
3. Отправляете запрос и всё, пароль изменен, вам придет письмо с уведомлением о смене пароляю
Expand Down
7 changes: 4 additions & 3 deletions auth_backend/auth_plugins/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from auth_backend.exceptions import AlreadyExists, AuthFailed, IncorrectUserAuthType, SessionExpired
from auth_backend.kafka.kafka import get_kafka_producer
from auth_backend.models.db import AuthMethod, User, UserSession
from auth_backend.schemas.types.password import Password
from auth_backend.schemas.types.scopes import Scope
from auth_backend.settings import get_settings
from auth_backend.utils.security import UnionAuth
Expand Down Expand Up @@ -70,7 +71,7 @@ class EmailLogin(Base):

class EmailRegister(Base):
email: Annotated[str, MinLen(1)]
password: Annotated[str, MinLen(1)]
password: Password
email_validator = field_validator("email")(check_email)


Expand All @@ -82,7 +83,7 @@ class EmailChange(Base):

class ResetPassword(Base):
password: Annotated[str, MinLen(1)]
new_password: Annotated[str, MinLen(1)]
new_password: Password

@model_validator(mode="after")
def check_passwords_dont_match(self) -> Self:
Expand All @@ -99,7 +100,7 @@ class RequestResetForgottenPassword(Base):


class ResetForgottenPassword(Base):
new_password: Annotated[str, MinLen(1)]
new_password: Password


class Email(UserdataMixin, LoginableMixin, RegistrableMixin, AuthPluginMeta):
Expand Down
2 changes: 2 additions & 0 deletions auth_backend/cli/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

from auth_backend.auth_plugins import Email
from auth_backend.models import AuthMethod, User
from auth_backend.schemas.types.password import validate_password
from auth_backend.utils.string import random_string


def create_user(email: str, password: str, session: Session) -> None:
password = validate_password(password)
if (
AuthMethod.query(session=session)
.filter(AuthMethod.value == email, AuthMethod.auth_method == "email")
Expand Down
56 changes: 56 additions & 0 deletions auth_backend/schemas/types/password.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import string
from typing import Any

from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler
from pydantic.json_schema import JsonSchemaValue
from pydantic_core import core_schema

PASSWORD_MIN_LENGTH = 8
PASSWORD_MAX_LENGTH = 32
PASSWORD_ALLOWED_CHARACTERS = string.ascii_letters + string.digits + string.punctuation
PASSWORD_PATTERN = r"^[\x21-\x7E]+$"
PASSWORD_REQUIREMENTS = (
f"Password must be {PASSWORD_MIN_LENGTH}-{PASSWORD_MAX_LENGTH} characters long and contain only "
"ASCII letters, digits and punctuation. Spaces and non-ASCII characters are not allowed."
)


def validate_password(value: str) -> str:
"""Validate a newly created password according to the Auth API password policy."""
if len(value) < PASSWORD_MIN_LENGTH:
raise ValueError(f"Password must be at least {PASSWORD_MIN_LENGTH} characters long")
if len(value) > PASSWORD_MAX_LENGTH:
raise ValueError(f"Password must be at most {PASSWORD_MAX_LENGTH} characters long")
if any(character not in PASSWORD_ALLOWED_CHARACTERS for character in value):
raise ValueError(
"Password may contain only ASCII letters, digits and punctuation; "
"spaces and non-ASCII characters are not allowed"
)
return value


class Password:
"""Pydantic type for a password that is being created or replaced."""

@classmethod
def __get_pydantic_core_schema__(
cls,
source: type[Any],
handler: GetCoreSchemaHandler,
) -> core_schema.CoreSchema:
return core_schema.no_info_after_validator_function(validate_password, core_schema.str_schema())

@classmethod
def __get_pydantic_json_schema__(
cls, core_schema_: core_schema.CoreSchema, handler: GetJsonSchemaHandler
) -> JsonSchemaValue:
field_schema = handler(core_schema_)
field_schema.update(
type="string",
format="password",
minLength=PASSWORD_MIN_LENGTH,
maxLength=PASSWORD_MAX_LENGTH,
pattern=PASSWORD_PATTERN,
description=PASSWORD_REQUIREMENTS,
)
return field_schema
4 changes: 2 additions & 2 deletions tests/test_routes/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def dbsession():
@pytest.fixture()
def user_id(client_auth: TestClient, dbsession):
time = datetime.datetime.utcnow()
body = {"email": f"user{time}@example.com", "password": "string"}
body = {"email": f"user{time}@example.com", "password": "string12"}
client_auth.post("/email/registration", json=body)
db_user: AuthMethod = (
dbsession.query(AuthMethod).filter(AuthMethod.value == body['email'], AuthMethod.param == 'email').one()
Expand All @@ -78,7 +78,7 @@ def user_id(client_auth: TestClient, dbsession):
def user(client_auth: TestClient, dbsession):
url = "/email/login"
time = datetime.datetime.utcnow()
body = {"email": f"user{time}@example.com", "password": "string", "scopes": []}
body = {"email": f"user{time}@example.com", "password": "string12", "scopes": []}
response = client_auth.post("/email/registration", json=body)
db_user: AuthMethod = (
dbsession.query(AuthMethod).filter(AuthMethod.value == body['email'], AuthMethod.param == 'email').one()
Expand Down
44 changes: 36 additions & 8 deletions tests/test_routes/test_change_password.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,14 @@ def test_unprocessable_jsons_with_token(client_auth: TestClient, dbsession: Sess
response = client_auth.post(
f"{url}/request",
headers={"Authorization": auth_token},
json={"password": "", "new_password": "changed"},
json={"password": "", "new_password": "changed12"},
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY

response = client_auth.post(
f"{url}/request",
headers={"Authorization": auth_token},
json={"password": "", "new_password": "changed"},
json={"password": "", "new_password": "changed12"},
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY

Expand All @@ -83,7 +83,21 @@ def test_unprocessable_jsons_with_token(client_auth: TestClient, dbsession: Sess
response = client_auth.post(
f"{url}/request",
headers={"Authorization": auth_token},
json={"password": body["password"], "new_password": "changed"},
json={"password": body["password"], "new_password": "short7"},
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY

response = client_auth.post(
f"{url}/request",
headers={"Authorization": auth_token},
json={"password": body["password"], "new_password": "пароль123"},
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY

response = client_auth.post(
f"{url}/request",
headers={"Authorization": auth_token},
json={"password": body["password"], "new_password": "changed12"},
)
assert response.status_code == status.HTTP_200_OK

Expand Down Expand Up @@ -113,6 +127,20 @@ def test_no_token(client_auth: TestClient, dbsession: Session, user_id: int):
assert reset_token
auth_params = Email.get_auth_method_params(user_id, session=dbsession)

response = client_auth.post(
f"{url}",
headers={"reset-token": reset_token.value},
json={"new_password": "short7"},
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY

response = client_auth.post(
f"{url}",
headers={"reset-token": reset_token.value},
json={"new_password": "пароль123"},
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY

response = client_auth.post(
f"{url}",
headers={"reset-token": reset_token.value + "x"},
Expand All @@ -129,7 +157,7 @@ def test_no_token(client_auth: TestClient, dbsession: Session, user_id: int):

response = client_auth.post(
"/email/login",
json={"email": auth_params["email"].value, "password": "string", "scopes": []},
json={"email": auth_params["email"].value, "password": "string12", "scopes": []},
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED

Expand All @@ -147,21 +175,21 @@ def test_with_token(client_auth: TestClient, dbsession: Session, user):
response = client_auth.post(
f"{url}/request",
headers={"Authorization": auth_token},
json={"password": "wrong", "new_password": "changed"},
json={"password": "wrong", "new_password": "changed12"},
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED

response = client_auth.post(
f"{url}/request",
headers={"Authorization": auth_token + "wrong"},
json={"password": body["password"], "new_password": "changed"},
json={"password": body["password"], "new_password": "changed12"},
)
assert response.status_code == status.HTTP_403_FORBIDDEN

response = client_auth.post(
f"{url}/request",
headers={"Authorization": auth_token},
json={"password": body["password"], "new_password": "changed"},
json={"password": body["password"], "new_password": "changed12"},
)
assert response.status_code == status.HTTP_200_OK
reset_token = (
Expand All @@ -175,7 +203,7 @@ def test_with_token(client_auth: TestClient, dbsession: Session, user):
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED

response = client_auth.post("/email/login", json={"email": body["email"], "password": "changed", "scopes": []})
response = client_auth.post("/email/login", json={"email": body["email"], "password": "changed12", "scopes": []})
assert response.status_code == status.HTTP_200_OK


Expand Down
4 changes: 2 additions & 2 deletions tests/test_routes/test_email_message_delay.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ def test_message_delay(client_auth_email_delay: TestClient, dbsession: Session):
settings_.EMAIL_DELAY_TIME_IN_MINUTES = 1
for i in range(settings.IP_DELAY_COUNT):
response = client_auth_email_delay.post(
"/email/registration", json={"email": f"test-user@profcomff.com", "password": "string"}
"/email/registration", json={"email": f"test-user@profcomff.com", "password": "string12"}
)
assert response.status_code == status.HTTP_200_OK
delay_response = client_auth_email_delay.post(
"/email/registration", json={"email": f"test-user@profcomff.com", "password": "string"}
"/email/registration", json={"email": f"test-user@profcomff.com", "password": "string12"}
)
assert delay_response.status_code == status.HTTP_429_TOO_MANY_REQUESTS
settings_.IP_DELAY_TIME_IN_MINUTES = ip_delay
Expand Down
8 changes: 4 additions & 4 deletions tests/test_routes/test_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@


def test_invalid_email(client: TestClient):
body = {"email": "some_string", "password": "string"}
body = {"email": "some_string", "password": "string12"}
response = client.post(url, json=body)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY

Expand All @@ -19,7 +19,7 @@ def test_main_scenario(client_auth: TestClient, dbsession: Session, user):
user_id, body, response = user["user_id"], user["body"], user["login_json"]
body_with_uppercase = {
"email": body["email"].replace("u", "U"),
"password": "string",
"password": "string12",
"scopes": [],
"session_name": "name",
}
Expand All @@ -28,8 +28,8 @@ def test_main_scenario(client_auth: TestClient, dbsession: Session, user):


def test_incorrect_data(client_auth: TestClient, dbsession: Session):
body1 = {"email": f"user{datetime.datetime.utcnow()}@example.com", "password": "string", "scopes": []}
body2 = {"email": "wrong@example.com", "password": "string", "scopes": []}
body1 = {"email": f"user{datetime.datetime.utcnow()}@example.com", "password": "string12", "scopes": []}
body2 = {"email": "wrong@example.com", "password": "string12", "scopes": []}
body3 = {"email": "some@example.com", "password": "strong", "scopes": []}
body4 = {"email": "wrong@example.com", "password": "strong", "scopes": []}
client_auth.post("/email/registration", json=body1)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_routes/test_logout.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@


def test_main_scenario(client_auth: TestClient, dbsession: Session):
body = {"email": f"user{datetime.utcnow()}@example.com", "password": "string", "scopes": []}
body = {"email": f"user{datetime.utcnow()}@example.com", "password": "string12", "scopes": []}
user_response = client_auth.post("/email/registration", json=body)
query = (
dbsession.query(AuthMethod)
Expand Down
4 changes: 2 additions & 2 deletions tests/test_routes/test_oidc.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def test_jwks(client_auth: TestClient):

def test_token_from_token_ok(client_auth: TestClient, dbsession: Session):
# Подготовка к тесту
body = {"email": f"user{datetime.utcnow()}@example.com", "password": "string", "scopes": []}
body = {"email": f"user{datetime.utcnow()}@example.com", "password": "string12", "scopes": []}
user_response = client_auth.post("/email/registration", json=body)
query = (
dbsession.query(AuthMethod)
Expand Down Expand Up @@ -108,7 +108,7 @@ def test_token_from_creds_ok(client_auth: TestClient, user):
"grant_type": "client_credentials",
"client_id": "app",
"username": user["body"]["email"],
"password": "string",
"password": "string12",
},
)
assert response.status_code == status.HTTP_200_OK
Expand Down
Loading
Loading