diff --git a/app/classes/exceptions/autherrors.py b/app/classes/exceptions/autherrors.py new file mode 100644 index 00000000..5825980e --- /dev/null +++ b/app/classes/exceptions/autherrors.py @@ -0,0 +1,2 @@ +class AuthError(Exception): + pass diff --git a/app/classes/web/routes/api/api_handlers.py b/app/classes/web/routes/api/api_handlers.py index df679e47..389e08a9 100644 --- a/app/classes/web/routes/api/api_handlers.py +++ b/app/classes/web/routes/api/api_handlers.py @@ -67,6 +67,7 @@ from app.classes.web.routes.api.users.user.permissions import ( from app.classes.web.routes.api.users.user.otp import ( APIUsersTOTPHandler, APIUsersTOTPIndexHandler, + APIUsersTOTPRecovery, ) from app.classes.web.routes.api.users.user.api import ApiUsersUserKeyHandler from app.classes.web.routes.api.users.user.pfp import ApiUsersUserPfpHandler @@ -172,6 +173,16 @@ def api_handlers(handler_args): ApiUsersUserIndexHandler, handler_args, ), + ( + r"/api/v2/users/(@me)/totp/recovery/renew/?", + APIUsersTOTPRecovery, + handler_args, + ), + ( + r"/api/v2/users/([0-9]+)/totp/recovery/renew/?", + APIUsersTOTPRecovery, + handler_args, + ), ( r"/api/v2/users/([0-9]+)/totp/?", APIUsersTOTPIndexHandler, diff --git a/app/classes/web/routes/api/auth/login.py b/app/classes/web/routes/api/auth/login.py index 889b0224..07222ecd 100644 --- a/app/classes/web/routes/api/auth/login.py +++ b/app/classes/web/routes/api/auth/login.py @@ -118,6 +118,7 @@ class ApiAuthLoginHandler(BaseApiHandler): totp_enabled = len(list(user_data.totp_user)) > 0 # Check if user has TOTP and if we got any type of TOTP data in the login # payload + valid_backup_code = False if totp_enabled and not totp and backup_code: # Check for backup code lowered_backup_code = str(backup_code).replace("-", "").lower() @@ -131,9 +132,10 @@ class ApiAuthLoginHandler(BaseApiHandler): valid_backup_code = code break try: - self.controller.totp.remove_recovery_code( - user_data.user_id, valid_backup_code - ) + if valid_backup_code: + self.controller.totp.remove_recovery_code( + user_data.user_id, valid_backup_code + ) except RuntimeError: self.finish_json( 401, @@ -152,7 +154,7 @@ class ApiAuthLoginHandler(BaseApiHandler): ) # Check if both password auth and totp auth passed login_result = pass_login_result is True and totp_login_result is True - elif not totp_enabled and not totp: + elif (not totp_enabled and not totp) and (not totp_enabled and not backup_code): # If the user doesn't have TOTP enabled and they didn't send a TOTP code # We'll pass them through login_result = pass_login_result @@ -178,7 +180,27 @@ class ApiAuthLoginHandler(BaseApiHandler): ) token = self.controller.authentication.generate(user_data.user_id) self.set_current_user(user_data.user_id, token) - self.finish_json( + if valid_backup_code: + return self.finish_json( + 200, + { + "status": "ok", + "data": { + "token": token, + "user_id": str(user_data.user_id), + "page": "/panel/dashboard", + "warning": self.helper.translation.translate( + "login", + "burnedBackupCode", + self.controller.users.get_user_lang_by_id( + user_data.user_id + ), + ), + }, + }, + ) + + return self.finish_json( 200, { "status": "ok", diff --git a/app/classes/web/routes/api/users/user/otp.py b/app/classes/web/routes/api/users/user/otp.py index 7bba83ef..f81b98d2 100644 --- a/app/classes/web/routes/api/users/user/otp.py +++ b/app/classes/web/routes/api/users/user/otp.py @@ -251,3 +251,50 @@ class APIUsersTOTPHandler(BaseApiHandler): 200, {"status": "ok"}, ) + + +class APIUsersTOTPRecovery(BaseApiHandler): + def get(self, user_id: str, totp_id=None): + auth_data = self.authenticate_user() + if not auth_data: + return + ( + _, + exec_user_crafty_permissions, + _, + _, + user, + _, + ) = auth_data + if totp_id: + return + + if user_id in ["@me", user["user_id"]]: + user_id = user["user_id"] + res_user = self.controller.users.get_user_object(user_id) + elif EnumPermissionsCrafty.USER_CONFIG not in exec_user_crafty_permissions: + return self.finish_json( + 400, + { + "status": "error", + "error": "NOT_AUTHORIZED", + }, + ) + else: + # has User_Config permission and isn't viewing self + res_user = self.controller.users.get_user_object(user_id) + if not res_user: + return self.finish_json( + 404, + { + "status": "error", + "error": "USER_NOT_FOUND", + }, + ) + + user_totp = list(res_user.recovery_user) + + self.finish_json( + 200, + {"status": "ok", "data": {"backup_codes": user_totp}}, + ) diff --git a/app/frontend/templates/panel/panel_edit_user_otp.html b/app/frontend/templates/panel/panel_edit_user_otp.html index 2bd616d0..1587e033 100644 --- a/app/frontend/templates/panel/panel_edit_user_otp.html +++ b/app/frontend/templates/panel/panel_edit_user_otp.html @@ -55,7 +55,11 @@