Added auth service stuff with jwt #70

This commit is contained in:
Sven Heidemann 2022-10-15 00:42:55 +02:00
parent 1090e502c2
commit 6f95d0a055
4 changed files with 204 additions and 77 deletions

View File

@ -33,11 +33,29 @@ class AuthServiceABC(ABC):
async def add_auth_user_async(self, user_dto: AuthUserDTO) -> int: pass async def add_auth_user_async(self, user_dto: AuthUserDTO) -> int: pass
@abstractmethod @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 @abstractmethod
async def login_async(self, user_dto: AuthUserDTO) -> TokenDTO: pass 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 @abstractmethod
async def forgot_password_async(self, email: str): pass async def forgot_password_async(self, email: str): pass
@ -46,21 +64,3 @@ class AuthServiceABC(ABC):
@abstractmethod @abstractmethod
async def reset_password_async(self, rp_dto: ResetPasswordDTO): pass 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

View File

@ -37,25 +37,49 @@ class AuthUserDTO(DtoABC):
def first_name(self) -> str: def first_name(self) -> str:
return self._first_name return self._first_name
@first_name.setter
def first_name(self, value: str):
self._first_name = value
@property @property
def last_name(self) -> str: def last_name(self) -> str:
return self._last_name return self._last_name
@last_name.setter
def last_name(self, value: str):
self._last_name = value
@property @property
def email(self) -> str: def email(self) -> str:
return self._email return self._email
@email.setter
def email(self, value: str):
self._email = value
@property @property
def password(self) -> str: def password(self) -> str:
return self._password return self._password
@password.setter
def password(self, value: str):
self._password = value
@property @property
def is_confirmed(self) -> bool: def is_confirmed(self) -> Optional[str]:
return self._is_confirmed return self._is_confirmed
@is_confirmed.setter
def is_confirmed(self, value: Optional[str]):
self._is_confirmed = value
@property @property
def auth_role(self) -> AuthRoleEnum: 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): def from_dict(self, values: dict):
self._id = values['Id'] self._id = values['Id']

View File

@ -7,12 +7,26 @@ from bot_api.abc.dto_abc import DtoABC
class TokenDTO(DtoABC): class TokenDTO(DtoABC):
def __init__(self): def __init__(self, token: str, refresh_token: str):
DtoABC.__init__(self) 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): def from_dict(self, values: dict):
pass self._token = values['Token']
self._refresh_token = values['RefreshToken']
def to_dict(self) -> dict: def to_dict(self) -> dict:
return { return {
'Token': self._token,
'RefreshToken': self._refresh_token
} }

View File

@ -1,18 +1,21 @@
import hashlib import hashlib
import re import re
import uuid import uuid
from datetime import datetime, timedelta, timezone
from typing import Optional from typing import Optional
import jwt
from cpl_core.database.context import DatabaseContextABC from cpl_core.database.context import DatabaseContextABC
from cpl_core.mailing import EMailClientABC, EMail from cpl_core.mailing import EMailClientABC, EMail
from cpl_query.extension import List from cpl_query.extension import List
from cpl_translation import TranslatePipe from cpl_translation import TranslatePipe
from bot_api.abc.auth_service_abc import AuthServiceABC 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.configuration.frontend_settings import FrontendSettings
from bot_api.filter.auth_user_select_criteria import AuthUserSelectCriteria from bot_api.filter.auth_user_select_criteria import AuthUserSelectCriteria
from bot_api.logging.api_logger import ApiLogger 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.auth_user_filtered_result_dto import AuthUserFilteredResultDTO
from bot_api.model.email_string_dto import EMailStringDTO from bot_api.model.email_string_dto import EMailStringDTO
from bot_api.model.reset_password_dto import ResetPasswordDTO from bot_api.model.reset_password_dto import ResetPasswordDTO
@ -32,6 +35,7 @@ class AuthService(AuthServiceABC):
db: DatabaseContextABC, db: DatabaseContextABC,
mailer: EMailClientABC, mailer: EMailClientABC,
t: TranslatePipe, t: TranslatePipe,
auth_settings: AuthenticationSettings,
frontend_settings: FrontendSettings, frontend_settings: FrontendSettings,
): ):
@ -42,6 +46,7 @@ class AuthService(AuthServiceABC):
self._db = db self._db = db
self._mailer = mailer self._mailer = mailer
self._t = t self._t = t
self._auth_settings = auth_settings
self._frontend_settings = frontend_settings self._frontend_settings = frontend_settings
@staticmethod @staticmethod
@ -61,6 +66,29 @@ class AuthService(AuthServiceABC):
regex = '^[a-z0-9]+[\\._]?[a-z0-9]+[@]\\w+[.]\\w{2,3}$' regex = '^[a-z0-9]+[\\._]?[a-z0-9]+[@]\\w+[.]\\w{2,3}$'
return bool(re.search(regex, email)) 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): def _send_confirmation_id_to_user(self, user: AuthUser):
url = self._frontend_settings.url url = self._frontend_settings.url
if not url.endswith('/'): if not url.endswith('/'):
@ -109,49 +137,24 @@ class AuthService(AuthServiceABC):
user = self._auth_users.find_auth_user_by_email(email) user = self._auth_users.find_auth_user_by_email(email)
return AUT.to_dto(user) if user is not None else None return AUT.to_dto(user) if user is not None else None
async def add_auth_user_async(self, user_dto: AuthUserDTO) -> AuthUser: async def add_auth_user_async(self, user_dto: AuthUserDTO):
pass 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_dto.password = self._hash_sha256(user_dto.password)
user = self._auth_users.find_auth_user_by_confirmation_id(id) user = AUT.to_db(user_dto)
if user is None: if not self._is_email_valid(user.email):
return False raise Exception('Invalid E-Mail address')
user.confirmation_id = None try:
self._auth_users.update_auth_user(user) user.confirmation_id = uuid.uuid4()
self._db.save_changes() self._auth_users.update_auth_user(user)
return True self._send_confirmation_id_to_user(user)
self._db.save_changes()
async def login_async(self, user_dto: AuthUserDTO) -> TokenDTO: self._logger.info(__name__, f'Added auth user with E-Mail: {user_dto.email}')
pass except Exception as e:
self._logger.error(__name__, f'Cannot add user with E-Mal {user_dto.email}', e)
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()
async def update_user_async(self, update_user_dto: UpdateAuthUserDTO): async def update_user_async(self, update_user_dto: UpdateAuthUserDTO):
if update_user_dto is None: if update_user_dto is None:
@ -258,12 +261,6 @@ class AuthService(AuthServiceABC):
self._db.save_changes() 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): async def delete_auth_user_by_email_async(self, email: str):
try: try:
user = self._auth_users.get_auth_user_by_email(email) user = self._auth_users.get_auth_user_by_email(email)
@ -280,3 +277,95 @@ class AuthService(AuthServiceABC):
except Exception as e: except Exception as e:
self._logger.error(__name__, f'Cannot delete user', e) self._logger.error(__name__, f'Cannot delete user', e)
raise Exception(f'Cannot delete user by mail {user_dto.email}') 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()