diff --git a/src/bot_api/abc/auth_service_abc.py b/src/bot_api/abc/auth_service_abc.py index 45788c9a..e4f611c6 100644 --- a/src/bot_api/abc/auth_service_abc.py +++ b/src/bot_api/abc/auth_service_abc.py @@ -33,11 +33,29 @@ class AuthServiceABC(ABC): async def add_auth_user_async(self, user_dto: AuthUserDTO) -> int: pass @abstractmethod - async def confirm_email_async(self, id: str) -> bool: pass + async def update_user_async(self, update_user_dto: UpdateAuthUserDTO): pass + + @abstractmethod + async def update_user_as_admin_async(self, update_user_dto: UpdateAuthUserDTO): pass + + @abstractmethod + async def delete_auth_user_by_email_async(self, email: str): pass + + @abstractmethod + async def delete_auth_user_async(self, user_dto: AuthUserDTO): pass @abstractmethod async def login_async(self, user_dto: AuthUserDTO) -> TokenDTO: pass + @abstractmethod + async def refresh_async(self, token_dto: TokenDTO) -> TokenDTO: pass + + @abstractmethod + async def revoke_async(self, token_dto: TokenDTO): pass + + @abstractmethod + async def confirm_email_async(self, id: str) -> bool: pass + @abstractmethod async def forgot_password_async(self, email: str): pass @@ -46,21 +64,3 @@ class AuthServiceABC(ABC): @abstractmethod async def reset_password_async(self, rp_dto: ResetPasswordDTO): pass - - @abstractmethod - async def update_user_async(self, update_user_dto: UpdateAuthUserDTO): pass - - @abstractmethod - async def update_user_as_admin_async(self, update_user_dto: UpdateAuthUserDTO): pass - - @abstractmethod - async def refresh_async(self, token_dto: TokenDTO) -> TokenDTO: pass - - @abstractmethod - async def revoke_async(self, token_dto: TokenDTO): pass - - @abstractmethod - async def delete_auth_user_by_email_async(self, email: str): pass - - @abstractmethod - async def delete_auth_user_async(self, user_dto: AuthUserDTO): pass diff --git a/src/bot_api/model/auth_user_dto.py b/src/bot_api/model/auth_user_dto.py index 8d7bce2b..304ce047 100644 --- a/src/bot_api/model/auth_user_dto.py +++ b/src/bot_api/model/auth_user_dto.py @@ -32,30 +32,54 @@ class AuthUserDTO(DtoABC): @property def id(self) -> int: return self._id - + @property def first_name(self) -> str: return self._first_name - + + @first_name.setter + def first_name(self, value: str): + self._first_name = value + @property def last_name(self) -> str: return self._last_name - + + @last_name.setter + def last_name(self, value: str): + self._last_name = value + @property def email(self) -> str: return self._email - + + @email.setter + def email(self, value: str): + self._email = value + @property def password(self) -> str: return self._password - + + @password.setter + def password(self, value: str): + self._password = value + @property - def is_confirmed(self) -> bool: + def is_confirmed(self) -> Optional[str]: return self._is_confirmed - + + @is_confirmed.setter + def is_confirmed(self, value: Optional[str]): + self._is_confirmed = value + @property def auth_role(self) -> AuthRoleEnum: - return self._auth_role + return self.auth_role + + @auth_role.setter + def auth_role(self, value: AuthRoleEnum): + self.auth_role = value def from_dict(self, values: dict): self._id = values['Id'] diff --git a/src/bot_api/model/token_dto.py b/src/bot_api/model/token_dto.py index 445943c2..987d9ad2 100644 --- a/src/bot_api/model/token_dto.py +++ b/src/bot_api/model/token_dto.py @@ -7,12 +7,26 @@ from bot_api.abc.dto_abc import DtoABC class TokenDTO(DtoABC): - def __init__(self): + def __init__(self, token: str, refresh_token: str): DtoABC.__init__(self) + self._token = token + self._refresh_token = refresh_token + + @property + def token(self) -> str: + return self._token + + @property + def refresh_token(self) -> str: + return self._refresh_token + def from_dict(self, values: dict): - pass + self._token = values['Token'] + self._refresh_token = values['RefreshToken'] def to_dict(self) -> dict: return { + 'Token': self._token, + 'RefreshToken': self._refresh_token } diff --git a/src/bot_api/service/auth_service.py b/src/bot_api/service/auth_service.py index 05283d4e..47258590 100644 --- a/src/bot_api/service/auth_service.py +++ b/src/bot_api/service/auth_service.py @@ -1,18 +1,21 @@ import hashlib import re import uuid +from datetime import datetime, timedelta, timezone from typing import Optional +import jwt from cpl_core.database.context import DatabaseContextABC from cpl_core.mailing import EMailClientABC, EMail from cpl_query.extension import List from cpl_translation import TranslatePipe from bot_api.abc.auth_service_abc import AuthServiceABC +from bot_api.configuration.authentication_settings import AuthenticationSettings from bot_api.configuration.frontend_settings import FrontendSettings from bot_api.filter.auth_user_select_criteria import AuthUserSelectCriteria from bot_api.logging.api_logger import ApiLogger -from bot_api.model.auth_user import AuthUserDTO +from bot_api.model.auth_user_dto import AuthUserDTO from bot_api.model.auth_user_filtered_result_dto import AuthUserFilteredResultDTO from bot_api.model.email_string_dto import EMailStringDTO from bot_api.model.reset_password_dto import ResetPasswordDTO @@ -32,6 +35,7 @@ class AuthService(AuthServiceABC): db: DatabaseContextABC, mailer: EMailClientABC, t: TranslatePipe, + auth_settings: AuthenticationSettings, frontend_settings: FrontendSettings, ): @@ -42,6 +46,7 @@ class AuthService(AuthServiceABC): self._db = db self._mailer = mailer self._t = t + self._auth_settings = auth_settings self._frontend_settings = frontend_settings @staticmethod @@ -61,6 +66,29 @@ class AuthService(AuthServiceABC): regex = '^[a-z0-9]+[\\._]?[a-z0-9]+[@]\\w+[.]\\w{2,3}$' return bool(re.search(regex, email)) + def _generate_token(self, user: AuthUser) -> str: + token = jwt.encode( + payload={ + 'user_id': user.id, + 'email': user.email, + 'role': user.auth_role, + 'exp': datetime.now(tz=timezone.utc) + timedelta(days=self._auth_settings.token_expire_time), + 'iss': self._auth_settings.issuer, + 'aud': self._auth_settings.audience + }, + key=self._auth_settings.secret_key, + ) + + return token + + def _create_and_save_refresh_token(self, user: AuthUser) -> str: + token = str(uuid.uuid4()) + user.refresh_token = token + user.refresh_token_expire_time = datetime.now(tz=timezone.utc) + timedelta(days=self._auth_settings.refresh_token_expire_time) + self._auth_users.update_auth_user(user) + self._db.save_changes() + return token + def _send_confirmation_id_to_user(self, user: AuthUser): url = self._frontend_settings.url if not url.endswith('/'): @@ -109,49 +137,24 @@ class AuthService(AuthServiceABC): user = self._auth_users.find_auth_user_by_email(email) return AUT.to_dto(user) if user is not None else None - async def add_auth_user_async(self, user_dto: AuthUserDTO) -> AuthUser: - pass + async def add_auth_user_async(self, user_dto: AuthUserDTO): + db_user = self._auth_users.find_auth_user_by_email(user_dto.email) + if db_user is not None: + raise Exception('User already exists') - async def confirm_email_async(self, id: str) -> bool: - user = self._auth_users.find_auth_user_by_confirmation_id(id) - if user is None: - return False + user_dto.password = self._hash_sha256(user_dto.password) + user = AUT.to_db(user_dto) + if not self._is_email_valid(user.email): + raise Exception('Invalid E-Mail address') - user.confirmation_id = None - self._auth_users.update_auth_user(user) - self._db.save_changes() - return True - - async def login_async(self, user_dto: AuthUserDTO) -> TokenDTO: - pass - - async def forgot_password_async(self, email: str): - user = self._auth_users.find_auth_user_by_email(email) - if user is None: - return - - user.forgot_password_id = uuid.uuid4() - self._auth_users.update_auth_user(user) - self._send_forgot_password_id_to_user(user) - self._db.save_changes() - - async def confirm_forgot_password_async(self, id: str) -> EMailStringDTO: - user = self._auth_users.find_auth_user_by_forgot_password_id(id) - return EMailStringDTO(user.email) - - async def reset_password_async(self, rp_dto: ResetPasswordDTO): - user = self._auth_users.find_auth_user_by_forgot_password_id(rp_dto.id) - if user is None: - pass - - if user.confirmation_id is not None: - pass - - if user.password is None or rp_dto.password == '': - pass - - user.password = self._hash_sha256(rp_dto.password) - self._db.save_changes() + try: + user.confirmation_id = uuid.uuid4() + self._auth_users.update_auth_user(user) + self._send_confirmation_id_to_user(user) + self._db.save_changes() + self._logger.info(__name__, f'Added auth user with E-Mail: {user_dto.email}') + except Exception as e: + self._logger.error(__name__, f'Cannot add user with E-Mal {user_dto.email}', e) async def update_user_async(self, update_user_dto: UpdateAuthUserDTO): if update_user_dto is None: @@ -258,12 +261,6 @@ class AuthService(AuthServiceABC): self._db.save_changes() - async def refresh_async(self, token_dto: TokenDTO) -> AuthUser: - pass - - async def revoke_async(self, token_dto: TokenDTO) -> AuthUser: - pass - async def delete_auth_user_by_email_async(self, email: str): try: user = self._auth_users.get_auth_user_by_email(email) @@ -280,3 +277,95 @@ class AuthService(AuthServiceABC): except Exception as e: self._logger.error(__name__, f'Cannot delete user', e) raise Exception(f'Cannot delete user by mail {user_dto.email}') + + async def login_async(self, user_dto: AuthUserDTO) -> TokenDTO: + if user_dto is None: + raise Exception('User not set') + + db_user = self._auth_users.find_auth_user_by_email(user_dto.email) + if db_user is None: + raise Exception(f'User with E-Mail {user_dto.email} not found') + + user_dto.password = self._hash_sha256(user_dto.password) + if db_user.password != user_dto.password: + raise Exception('Wrong password') + + token = self._generate_token(user_dto) + refresh_token = self._create_and_save_refresh_token(db_user) + if db_user.forgot_password_id is not None: + db_user.forgot_password_id = None + + self._db.save_changes() + return TokenDTO(token, refresh_token) + + async def refresh_async(self, token_dto: TokenDTO) -> TokenDTO: + if token_dto is None: + raise Exception(f'Token not set') + + token = jwt.decode(token_dto.token, key=self._auth_settings.secret_key) + if token is None or 'email' not in token: + raise Exception('Token invalid') + + try: + user = self._auth_users.get_auth_user_by_email(token) + if user is None or user.refresh_token != token_dto.refresh_token or user.refresh_token_expire_time <= datetime.now(): + raise Exception('Token expired') + + return TokenDTO(self._generate_token(user), self._create_and_save_refresh_token(user)) + except Exception as e: + self._logger.error(__name__, f'Refreshing token failed', e) + return TokenDTO('', '') + + async def revoke_async(self, token_dto: TokenDTO): + if token_dto is None or token_dto.token is None or token_dto.refresh_token is None: + raise Exception('Token not set') + + token = jwt.decode(token_dto.token, key=self._auth_settings.secret_key) + try: + user = self._auth_users.get_auth_user_by_email(token) + if user is None or user.refresh_token != token_dto.refresh_token or user.refresh_token_expire_time <= datetime.now(): + raise Exception('Token expired') + + user.refresh_token = None + self._auth_users.update_auth_user(user) + self._db.save_changes() + except Exception as e: + self._logger.error(__name__, f'Refreshing token failed', e) + + async def confirm_email_async(self, id: str) -> bool: + user = self._auth_users.find_auth_user_by_confirmation_id(id) + if user is None: + return False + + user.confirmation_id = None + self._auth_users.update_auth_user(user) + self._db.save_changes() + return True + + async def forgot_password_async(self, email: str): + user = self._auth_users.find_auth_user_by_email(email) + if user is None: + return + + user.forgot_password_id = uuid.uuid4() + self._auth_users.update_auth_user(user) + self._send_forgot_password_id_to_user(user) + self._db.save_changes() + + async def confirm_forgot_password_async(self, id: str) -> EMailStringDTO: + user = self._auth_users.find_auth_user_by_forgot_password_id(id) + return EMailStringDTO(user.email) + + async def reset_password_async(self, rp_dto: ResetPasswordDTO): + user = self._auth_users.find_auth_user_by_forgot_password_id(rp_dto.id) + if user is None: + raise Exception(f'User by forgot password id {rp_dto.id} not found') + + if user.confirmation_id is not None: + raise Exception(f'E-Mail not confirmed') + + if user.password is None or rp_dto.password == '': + raise Exception(f'Password not set') + + user.password = self._hash_sha256(rp_dto.password) + self._db.save_changes()