From c53173987c8e0a8e08d7a10881af97437eb62e4b Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Mon, 7 Oct 2024 10:06:47 -0400 Subject: [PATCH] Refs #253. Updated login 2fa API paths in views. Work started on login - change password user API. --- .../API/user/UserLoginChangePassword.py | 50 +++++++++++++++++++ .../FlaskModule/API/user/UserQueryUsers.py | 5 +- .../python/modules/FlaskModule/FlaskModule.py | 6 ++- .../python/opentera/db/models/TeraUser.py | 20 +++++++- .../python/templates/login_setup_2fa.html | 4 +- .../python/templates/login_validate_2fa.html | 2 +- .../API/user/test_UserQueryUsers.py | 6 ++- 7 files changed, 84 insertions(+), 9 deletions(-) create mode 100644 teraserver/python/modules/FlaskModule/API/user/UserLoginChangePassword.py diff --git a/teraserver/python/modules/FlaskModule/API/user/UserLoginChangePassword.py b/teraserver/python/modules/FlaskModule/API/user/UserLoginChangePassword.py new file mode 100644 index 00000000..78eb2463 --- /dev/null +++ b/teraserver/python/modules/FlaskModule/API/user/UserLoginChangePassword.py @@ -0,0 +1,50 @@ +from modules.FlaskModule.API.user.UserLoginBase import UserLoginBase +from modules.FlaskModule.FlaskModule import user_api_ns as api +from modules.LoginModule.LoginModule import LoginModule, current_user +from opentera.db.models.TeraUser import TeraUser, UserPasswordInsecure, UserNewPasswordSameAsOld +from modules.FlaskModule.FlaskUtils import FlaskUtils + +from flask_babel import gettext +from flask import redirect + +post_parser = api.parser() +post_parser.add_argument('new_password', type=str, required=True, help='New password for the user') +post_parser.add_argument('confirm_password', type=str, required=True, help='Password confirmation for the user') + +class UserLoginChangePassword(UserLoginBase): + """ + UserLoginChangePassword endpoint resource. + """ + + @api.doc(description='Change password for the user. This API will only work if forced change is required on login. ' + 'Otherwise, use the standard \'api/user\' endpoint.') + @api.expect(post_parser, validate=True) + @LoginModule.user_session_required + def post(self): + """ + Change password for a user on login (forced change) + """ + try: + args = post_parser.parse_args(strict=True) + new_password = args['new_password'] + confirm_password = args['confirm_password'] + + # Validate if new password and confirm password are the same + if new_password != confirm_password: + return gettext('New password and confirm password do not match'), 400 + + # Change password, will be encrypted + # Will also reset force password change flag + try: + TeraUser.update(current_user.id_user, {'user_password': new_password, + 'user_force_password_change': False}) + except UserPasswordInsecure as e: + return FlaskUtils.get_password_weaknesses_text(e.weaknesses, '
'), 400 + except UserNewPasswordSameAsOld: + return gettext('New password same as old password'), 400 + + return redirect(self._generate_login_url()) + except Exception as e: + # Something went wrong, logout user + self._user_logout() + raise e diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryUsers.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryUsers.py index 72bb4084..0e42a4eb 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryUsers.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryUsers.py @@ -3,7 +3,7 @@ from sqlalchemy import exc from modules.LoginModule.LoginModule import user_multi_auth, current_user from modules.FlaskModule.FlaskModule import user_api_ns as api -from opentera.db.models.TeraUser import TeraUser, UserPasswordInsecure +from opentera.db.models.TeraUser import TeraUser, UserPasswordInsecure, UserNewPasswordSameAsOld from opentera.db.models.TeraUserGroup import TeraUserGroup from flask_babel import gettext from modules.DatabaseModule.DBManager import DBManager @@ -256,6 +256,9 @@ def post(self): except UserPasswordInsecure as e: return (gettext('Password not strong enough') + ': ' + FlaskUtils.get_password_weaknesses_text(e.weaknesses), 400) + except UserNewPasswordSameAsOld: + return gettext('New password same as old password'), 400 + else: # New user, check if password is set # if 'user_password' not in json_user: diff --git a/teraserver/python/modules/FlaskModule/FlaskModule.py b/teraserver/python/modules/FlaskModule/FlaskModule.py index 2211de20..6c1d1b86 100755 --- a/teraserver/python/modules/FlaskModule/FlaskModule.py +++ b/teraserver/python/modules/FlaskModule/FlaskModule.py @@ -142,6 +142,7 @@ def init_user_api(module: object, namespace: Namespace, additional_args: dict = from modules.FlaskModule.API.user.UserLogin import UserLogin from modules.FlaskModule.API.user.UserLogin2FA import UserLogin2FA from modules.FlaskModule.API.user.UserLoginSetup2FA import UserLoginSetup2FA + from modules.FlaskModule.API.user.UserLoginChangePassword import UserLoginChangePassword from modules.FlaskModule.API.user.UserLogout import UserLogout from modules.FlaskModule.API.user.UserQueryUsers import UserQueryUsers from modules.FlaskModule.API.user.UserQueryUserPreferences import UserQueryUserPreferences @@ -204,8 +205,9 @@ def init_user_api(module: object, namespace: Namespace, additional_args: dict = namespace.add_resource(UserQueryForms, '/forms', resource_class_kwargs=kwargs) namespace.add_resource(UserQueryParticipantGroup, '/groups', resource_class_kwargs=kwargs) namespace.add_resource(UserLogin, '/login', resource_class_kwargs=kwargs) - namespace.add_resource(UserLogin2FA, '/login_2fa', resource_class_kwargs=kwargs) - namespace.add_resource(UserLoginSetup2FA, '/login_setup_2fa', resource_class_kwargs=kwargs) + namespace.add_resource(UserLogin2FA, '/login/2fa', resource_class_kwargs=kwargs) + namespace.add_resource(UserLoginSetup2FA, '/login/setup_2fa', resource_class_kwargs=kwargs) + namespace.add_resource(UserLoginChangePassword, '/login/change_password', resource_class_kwargs=kwargs) namespace.add_resource(UserLogout, '/logout', resource_class_kwargs=kwargs) namespace.add_resource(UserQueryParticipants, '/participants', resource_class_kwargs=kwargs) namespace.add_resource(UserQueryOnlineParticipants, '/participants/online', resource_class_kwargs=kwargs) diff --git a/teraserver/python/opentera/db/models/TeraUser.py b/teraserver/python/opentera/db/models/TeraUser.py index aa640e61..5c3b75e6 100755 --- a/teraserver/python/opentera/db/models/TeraUser.py +++ b/teraserver/python/opentera/db/models/TeraUser.py @@ -328,13 +328,20 @@ def update(cls, id_user: int, values: dict): # Remove the password field is present and if empty if 'user_password' in values: if values['user_password'] == '': - del values['user_password'] + del values['user_password'] # Don't change password if empty else: # Check password strength password_errors = TeraUser.validate_password_strength(str(values['user_password'])) if len(password_errors) > 0: raise UserPasswordInsecure("User password insufficient strength", password_errors) + # Check that old password != new password + current_user = TeraUser.get_user_by_id(id_user) + if current_user: + if TeraUser.verify_password('', values['user_password'], current_user): + # Same password as before + raise UserNewPasswordSameAsOld("New password same as old") + # Forcing password to string values['user_password'] = TeraUser.encrypt_password(str(values['user_password'])) @@ -508,4 +515,13 @@ class PasswordWeaknesses(Enum): def __init__(self, message, weaknesses: list): super().__init__(message) - self.weaknesses = weaknesses \ No newline at end of file + self.weaknesses = weaknesses + + +class UserNewPasswordSameAsOld(Exception): + """ + Raised when the new password is equal to the old one + """ + def __init__(self, message): + super().__init__(message) + diff --git a/teraserver/python/templates/login_setup_2fa.html b/teraserver/python/templates/login_setup_2fa.html index 7b0bc8d3..7f1c4962 100644 --- a/teraserver/python/templates/login_setup_2fa.html +++ b/teraserver/python/templates/login_setup_2fa.html @@ -56,7 +56,7 @@ // Get the QR Code from the server $.ajax({ type: "GET", - url: "/api/user/login_setup_2fa", + url: "/api/user/login/setup_2fa", success: function(response) { console.log("QR Code received"); // Display the QR Code @@ -81,7 +81,7 @@ // Send the form data to the backend with a post request $.ajax({ type: "POST", - url: "/api/user/login_setup_2fa", + url: "/api/user/login/setup_2fa", data: form.serialize(), success: function(response) { console.log("2FA setup success"); diff --git a/teraserver/python/templates/login_validate_2fa.html b/teraserver/python/templates/login_validate_2fa.html index 2f312301..d976c484 100644 --- a/teraserver/python/templates/login_validate_2fa.html +++ b/teraserver/python/templates/login_validate_2fa.html @@ -90,7 +90,7 @@ // Use the login API for this purpose $.ajax({ type: "POST", - url: "/api/user/login_2fa", + url: "/api/user/login/2fa", data: { otp_code: otp_code, with_websocket: true diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryUsers.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryUsers.py index 48ee5ff8..8e029bb9 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryUsers.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryUsers.py @@ -412,11 +412,15 @@ def test_password_strength(self): json=json_data) self.assertEqual(400, response.status_code, msg="Password without numbers") - json_data['user']['user_password'] = 'Password12345!' + json_data['user']['user_password'] = 'Password12345!!' response = self._post_with_user_http_auth(self.test_client, username='admin', password='admin', json=json_data) self.assertEqual(200, response.status_code, msg="Password OK") + response = self._post_with_user_http_auth(self.test_client, username='admin', password='admin', + json=json_data) + self.assertEqual(400, response.status_code, msg="Password same as old") + TeraUser.delete(current_id) def test_post_and_delete(self):