From 0fb6d08a7f0608462d5cd72ac3e1b9151818c522 Mon Sep 17 00:00:00 2001 From: Daniele Maglie Date: Sun, 21 Jan 2024 14:18:57 +0100 Subject: [PATCH] Added sessions support. --- ioutils/errors.py | 18 ++++++----- ioutils/protected.py | 15 ++++++++-- tests/01_reporter/test_0004_ticket.py | 4 ++- tests/base.py | 5 ++-- v1/handlers/account/session/get_all.py | 41 ++++++++++++++++++++++++++ v1/handlers/authentication/login.py | 31 ++++--------------- v1/handlers/authentication/logout.py | 20 ++++++++++--- v1/handlers/authentication/refresh.py | 36 +++++++++++----------- v1/routes.py | 5 ++++ 9 files changed, 115 insertions(+), 60 deletions(-) create mode 100644 v1/handlers/account/session/get_all.py diff --git a/ioutils/errors.py b/ioutils/errors.py index 1ddadcc..12d202a 100644 --- a/ioutils/errors.py +++ b/ioutils/errors.py @@ -23,17 +23,19 @@ class ErrorCode: TOKEN_FORMAT_NON_VALID = '1008' - NON_VALID_PARAMETERS = '1009' + TOKEN_EXPIRED = '1009' - MISSING_PARAMETERS = '1010' + NON_VALID_PARAMETERS = '1010' - TOO_MANY_PARAMETERS = '1011' + MISSING_PARAMETERS = '1011' - MISSING_FILE = '1012' + TOO_MANY_PARAMETERS = '1012' - CHANGE_PASSWORD = '1013' + MISSING_FILE = '1013' - IP_ADDRESS_BLACKLISTED = '1014' + CHANGE_PASSWORD = '1014' + + IP_ADDRESS_BLACKLISTED = '1015' class ErrorMessage: @@ -63,6 +65,8 @@ class ErrorMessage: TOKEN_FORMAT_NON_VALID = 'Token format non valid.' + TOKEN_EXPIRED = 'This token is expired.' + # json POST parameters NON_VALID_PARAMETERS = 'Expecting JSON data.' @@ -77,6 +81,6 @@ class ErrorMessage: # account settings - CHANGE_PASSWORD = 'A password change has been activated for your account. You must first authenticate via web app and follow the instructions.' + CHANGE_PASSWORD = 'A password change has been activated for your account. You must authenticate via web interface and follow the instructions. You should now discard this tokens pairs.' IP_ADDRESS_BLACKLISTED = 'Your IP address is temporary blacklisted.' diff --git a/ioutils/protected.py b/ioutils/protected.py index 4464b03..3cc7458 100644 --- a/ioutils/protected.py +++ b/ioutils/protected.py @@ -2,6 +2,8 @@ from .base import BaseHandler from piracyshield_component.exception import ApplicationException +from piracyshield_service.security.blacklist.exists_by_access_token import SecurityBlacklistExistsByAccessTokenService + from piracyshield_service.authentication.verify_access_token import AuthenticationVerifyAccessTokenService from piracyshield_service.permission.service import PermissionService @@ -28,6 +30,8 @@ class ProtectedHandler(BaseHandler): account_data = {} async def prepare(self): + self.security_blacklist_exists_by_access_token_service = SecurityBlacklistExistsByAccessTokenService() + self.authentication_verify_access_token_service = AuthenticationVerifyAccessTokenService() await super().prepare() @@ -52,12 +56,19 @@ class ProtectedHandler(BaseHandler): return False # get the token only - token = authorization_header[7:] + self.current_access_token = authorization_header[7:] try: + if self.security_blacklist_exists_by_access_token_service.execute( + access_token = self.current_access_token + ) == True: + self.error(status_code = 401, error_code = ErrorCode.TOKEN_EXPIRED, message = ErrorMessage.TOKEN_EXPIRED) + + return False + # set the current account data # TODO: absolutely need to validate the payload. - self.account_data = self.authentication_verify_access_token_service.execute(token) + self.account_data = self.authentication_verify_access_token_service.execute(self.current_access_token) if not self.account_data.get('email'): self.error(status_code = 401, error_code = ErrorCode.TOKEN_FORMAT_NON_VALID, message = ErrorMessage.TOKEN_FORMAT_NON_VALID) diff --git a/tests/01_reporter/test_0004_ticket.py b/tests/01_reporter/test_0004_ticket.py index f00e602..3b1ca43 100644 --- a/tests/01_reporter/test_0004_ticket.py +++ b/tests/01_reporter/test_0004_ticket.py @@ -15,7 +15,7 @@ class TestReporterCreateTicket: ticket_wait_time = 76 ticket_parameters = { - 'dda_id': '2326485749e94573bf5724ff5006f30c', + 'dda_id': '002ad48ea02a43db9003b4f15f1da9b3', 'description': '__MOCK_TICKET__', 'forensic_evidence': { 'hash': {} @@ -44,6 +44,8 @@ class TestReporterCreateTicket: create_response = authenticated_post_request('/api/v1/ticket/create', self.access_token, self.ticket_parameters) + print(" RES -> ", create_response.json()) + assert create_response.status_code == 200 assert create_response.json()['status'] == 'success' diff --git a/tests/base.py b/tests/base.py index 3912611..26c7af8 100644 --- a/tests/base.py +++ b/tests/base.py @@ -4,9 +4,10 @@ import pytest import os import requests -application_config = Config('api').get('general') +application_config = Config('application').get('general') +api_config = Config('application').get('api') -URL = f"http://127.0.0.1:{application_config['port']}" +URL = f"{application_config.get('domain')}:{api_config.get('port')}" def get_request(endpoint: str): return requests.get(f'{URL}{endpoint}') diff --git a/v1/handlers/account/session/get_all.py b/v1/handlers/account/session/get_all.py new file mode 100644 index 0000000..0dcb8fd --- /dev/null +++ b/v1/handlers/account/session/get_all.py @@ -0,0 +1,41 @@ +import sys +import os + +# I hate python imports +current = os.path.dirname(os.path.realpath(__file__)) +parent = os.path.dirname(current) +sys.path.append(parent) + +import tornado + +from ioutils.protected import ProtectedHandler + +from piracyshield_service.permission.service import PermissionService +from piracyshield_service.account.session.get_all_by_account_ordered import AccountSessionGetAllByAccountOrderedService + +from piracyshield_component.exception import ApplicationException + +class GetAllSessionAccountHandler(ProtectedHandler): + + """ + Handles getting multiple active sessions. + """ + + async def get(self): + if self.initialize_account() == False: + return + + try: + account_session_get_all_by_account_ordered_service = AccountSessionGetAllByAccountOrderedService() + + response = await tornado.ioloop.IOLoop.current().run_in_executor( + None, + account_session_get_all_by_account_ordered_service.execute, + self.account_data.get('account_id'), + self.current_access_token + ) + + self.success(data = response) + + except ApplicationException as e: + self.error(status_code = 400, error_code = e.code, message = e.message) diff --git a/v1/handlers/authentication/login.py b/v1/handlers/authentication/login.py index 4709ca3..a58676d 100644 --- a/v1/handlers/authentication/login.py +++ b/v1/handlers/authentication/login.py @@ -11,8 +11,6 @@ from ioutils.base import BaseHandler from ioutils.errors import ErrorCode, ErrorMessage from piracyshield_service.authentication.authenticate import AuthenticationAuthenticateService -from piracyshield_service.authentication.generate_access_token import AuthenticationGenerateAccessTokenService -from piracyshield_service.authentication.generate_refresh_token import AuthenticationGenerateRefreshTokenService from piracyshield_component.exception import ApplicationException @@ -36,14 +34,19 @@ class AuthenticationLoginHandler(BaseHandler): return try: + authentication_authenticate_service = AuthenticationAuthenticateService() + access_token, refresh_token = await tornado.ioloop.IOLoop.current().run_in_executor( None, - self.process, + authentication_authenticate_service.execute, self.request_data.get('email'), self.request_data.get('password'), self.request.remote_ip ) + # store the refresh token in a http-only secure cookie + self.set_refresh_cookie(value = refresh_token) + self.success(data = { 'access_token': access_token, 'refresh_token': refresh_token @@ -51,25 +54,3 @@ class AuthenticationLoginHandler(BaseHandler): except ApplicationException as e: self.error(status_code = 400, error_code = e.code, message = e.message) - - def process(self, email: str, password: str, ip_address: str) -> tuple: - authentication_authenticate_service = AuthenticationAuthenticateService() - - # try to authenticate - payload = authentication_authenticate_service.execute( - email = email, - password = password, - ip_address = ip_address - ) - - authentication_generate_access_token_service = AuthenticationGenerateAccessTokenService() - authentication_generate_refresh_token_service = AuthenticationGenerateRefreshTokenService() - - # generate token pairs - access_token = authentication_generate_access_token_service.execute(payload) - refresh_token = authentication_generate_refresh_token_service.execute(payload) - - # store the refresh_token in a http-only cookie - self.set_refresh_cookie(value = refresh_token) - - return access_token, refresh_token diff --git a/v1/handlers/authentication/logout.py b/v1/handlers/authentication/logout.py index 46938bd..17fb8b0 100644 --- a/v1/handlers/authentication/logout.py +++ b/v1/handlers/authentication/logout.py @@ -7,19 +7,31 @@ sys.path.append(parent) from ioutils.protected import ProtectedHandler +from piracyshield_service.account.session.destroy_current_sessions import AccountSessionDestroyCurrentSessionsService + from piracyshield_component.exception import ApplicationException class AuthenticationLogoutHandler(ProtectedHandler): """ - Removes the authentication refresh token. - The effective logout remains on the access token expiration time, this is why it should be set to a short time. + Removes the authentication refresh token and blacklists both the tokens. """ def get(self): if self.initialize_account() == False: return - self.clear_cookie('refresh_token') + try: + account_session_destroy_current_sessions_service = AccountSessionDestroyCurrentSessionsService() - self.success(data = 'Goodbye!') + account_session_destroy_current_sessions_service.execute( + self.account_data.get('account_id'), + self.current_access_token + ) + + self.clear_cookie('refresh_token') + + self.success(data = 'Goodbye!') + + except ApplicationException as e: + self.error(status_code = 400, error_code = e.code, message = e.message) diff --git a/v1/handlers/authentication/refresh.py b/v1/handlers/authentication/refresh.py index 1942d59..4e1c4d2 100644 --- a/v1/handlers/authentication/refresh.py +++ b/v1/handlers/authentication/refresh.py @@ -10,8 +10,9 @@ import tornado from ioutils.base import BaseHandler from ioutils.errors import ErrorCode, ErrorMessage -from piracyshield_service.authentication.verify_refresh_token import AuthenticationVerifyRefreshTokenService -from piracyshield_service.authentication.generate_access_token import AuthenticationGenerateAccessTokenService +from piracyshield_service.security.blacklist.exists_by_refresh_token import SecurityBlacklistExistsByRefreshTokenService + +from piracyshield_service.authentication.refresh_access_token import AuthenticationRefreshAccessTokenService from piracyshield_component.exception import ApplicationException @@ -41,10 +42,22 @@ class AuthenticationRefreshHandler(BaseHandler): refresh_token = self.request_data.get('refresh_token') try: + security_blacklist_exists_by_refresh_token_service = SecurityBlacklistExistsByRefreshTokenService() + + if security_blacklist_exists_by_refresh_token_service.execute( + refresh_token = refresh_token + ) == True: + self.error(status_code = 401, error_code = ErrorCode.TOKEN_EXPIRED, message = ErrorMessage.TOKEN_EXPIRED) + + return False + + authentication_refresh_access_token_service = AuthenticationRefreshAccessTokenService() + access_token = await tornado.ioloop.IOLoop.current().run_in_executor( None, - self.process, - refresh_token + authentication_refresh_access_token_service.execute, + refresh_token, + self.request.remote_ip ) # return the access_token @@ -54,18 +67,3 @@ class AuthenticationRefreshHandler(BaseHandler): except ApplicationException as e: self.error(status_code = 400, error_code = e.code, message = e.message) - - def process(self, refresh_token: str) -> str: - authentication_verify_refresh_token_service = AuthenticationVerifyRefreshTokenService() - - # verify the token and unwrap the payload - payload = authentication_verify_refresh_token_service.execute( - token = refresh_token - ) - - authentication_generate_access_token_service = AuthenticationGenerateAccessTokenService() - - # generate a new access token - access_token = authentication_generate_access_token_service.execute(payload) - - return access_token diff --git a/v1/routes.py b/v1/routes.py index 13c9cd0..a4de81d 100644 --- a/v1/routes.py +++ b/v1/routes.py @@ -31,6 +31,8 @@ from .handlers.account.provider.set_status import SetStatusActiveProviderAccount from .handlers.account.provider.change_password import ChangePasswordProviderAccountHandler from .handlers.account.provider.remove import RemoveProviderAccountHandler +from .handlers.account.session.get_all import GetAllSessionAccountHandler + from .handlers.ticket.create import CreateTicketHandler from .handlers.ticket.get import GetTicketHandler from .handlers.ticket.get_all import GetAllTicketHandler @@ -139,6 +141,9 @@ class APIv1: (r"/account/provider/change_password", ChangePasswordProviderAccountHandler), (r"/account/provider/remove", RemoveProviderAccountHandler), + # sessions + (r"/account/session/get/all", GetAllSessionAccountHandler), + # blocking ticket management (r"/ticket/create", CreateTicketHandler), (r"/ticket/get", GetTicketHandler),