From a4c5324eeb24072db496679876a635392ace8d53 Mon Sep 17 00:00:00 2001 From: Niels Lachat Date: Fri, 18 Oct 2024 12:50:54 +0200 Subject: [PATCH] Start impl security tests --- auth_external/controllers/auth.py | 9 +- auth_external/tests/README.md | 4 + auth_external/tests/__init__.py | 1 + auth_external/tests/test_auth_controller.py | 109 ++++++++++++++++++++ 4 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 auth_external/tests/README.md create mode 100644 auth_external/tests/__init__.py create mode 100644 auth_external/tests/test_auth_controller.py diff --git a/auth_external/controllers/auth.py b/auth_external/controllers/auth.py index 91c6d5ba7..1b4292614 100644 --- a/auth_external/controllers/auth.py +++ b/auth_external/controllers/auth.py @@ -7,9 +7,12 @@ _logger = logging.getLogger(__name__) + +AUTH_LOGIN_ROUTE = "/auth/login" class AuthController(Controller): + @route( - route="/auth/login", + route=AUTH_LOGIN_ROUTE, auth="none", type="json", methods=["POST"], @@ -17,7 +20,7 @@ class AuthController(Controller): cors="*", ) def login(self): - username = request.jsonrequest["username"] + login = request.jsonrequest["login"] password = request.jsonrequest["password"] totp = request.jsonrequest["totp"] @@ -25,7 +28,7 @@ def login(self): res_users = registry(db)["res.users"] user_id = res_users.authenticate( - db, username, password, {"totp": totp, "interactive": False} + db, login, password, {"totp": totp, "interactive": False} ) user = request.env["res.users"].browse(int(user_id)) diff --git a/auth_external/tests/README.md b/auth_external/tests/README.md new file mode 100644 index 000000000..8d35ffd49 --- /dev/null +++ b/auth_external/tests/README.md @@ -0,0 +1,4 @@ +# Running the tests +```sh +odoo/odoo-bin -c etc/dev_t1486.conf -u auth_external -i auth_external --test-tags=auth_external --stop-after-init +``` \ No newline at end of file diff --git a/auth_external/tests/__init__.py b/auth_external/tests/__init__.py new file mode 100644 index 000000000..15cc7c2b9 --- /dev/null +++ b/auth_external/tests/__init__.py @@ -0,0 +1 @@ +from . import test_auth_controller \ No newline at end of file diff --git a/auth_external/tests/test_auth_controller.py b/auth_external/tests/test_auth_controller.py new file mode 100644 index 000000000..8bb7d8cf8 --- /dev/null +++ b/auth_external/tests/test_auth_controller.py @@ -0,0 +1,109 @@ +import json +from odoo.tests.common import HttpCase +from ..controllers.auth import AUTH_LOGIN_ROUTE +from http import HTTPStatus +from requests import Response + + +class TestAuthController(HttpCase): + TEST_USER_NORMAL = { + "login": "user_normal", + "password": "password_normal", + } + + TEST_USER_2FA_CREATE = { + "login": "user_2fa", + "password": "password_2fa", + "totp_secret": "totp_secret", + "totp_enabled": True + } + + TEST_USER_2FA = { + "login": TEST_USER_2FA_CREATE["login"], + "password": TEST_USER_2FA_CREATE["password"], + "totp": "123456" + } + + def setUp(self, *args, **kwargs): + super(TestAuthController, self).setUp(*args, **kwargs) + res_users = self.env["res.users"] + self.test_user_normal = res_users.create( + TestAuthController.TEST_USER_NORMAL + ) + self.test_user_2fa = res_users.create( + TestAuthController.TEST_USER_2FA_CREATE + ) + + def json_post(self, route: str, data: dict) -> Response: + JSON_HEADERS = {"Content-Type": "application/json"} + return self.url_open(route, data=json.dumps(data), headers=JSON_HEADERS) + + def assert_access_denied(self, response: Response) -> None: + self.assertEqual(response.status_code, HTTPStatus.OK) + response_data = json.loads(response.text) + self.assertEqual( + response_data["error"]["data"]["name"], "odoo.exceptions.AccessDenied" + ) + + def should_deny_access(self, login_data: dict) -> None: + response = self.json_post(AUTH_LOGIN_ROUTE, login_data) + self.assert_access_denied(response) + + def test_login_should_fail_with_invalid_user(self): + data = { + "login": "This username should not exist", + # and if it does, it's a really weird edge case + "password": "password", + "totp": "123456", + } + self.should_deny_access(data) + + def test_login_should_succeed_for_normal_user(self): + response = self.json_post(AUTH_LOGIN_ROUTE, TestAuthController.TEST_USER_NORMAL) + print(response.text) + self.assertTrue(False) + + def test_access_denied_incorrect_password(self): + """ + An attacker is denied access to a normal user account when providing an incorrect password + """ + data_incorrect_password = TestAuthController.TEST_USER_NORMAL.copy() + data_incorrect_password["password"] = "incorrect_password" + self.should_deny_access(data_incorrect_password) + + def test_access_denied_2fa_correct_password_absent_totp(self): + """ + An attacker is denied access to a 2fa user account when not providing any totp + """ + data = TestAuthController.TEST_USER_2FA.copy() + del data["totp"] + self.should_deny_access(data) + + def test_access_denied_2fa_correct_password_incorrect_totp(self): + """ + An attacker is denied access to a 2fa user account when providing a correct password but incorrect totp + """ + data_incorrect_totp = TestAuthController.TEST_USER_2FA.copy() + data_incorrect_totp["totp"] = "123456" # 1/1'000'000 chance that this is the correct totp and that the test fails + self.should_deny_access(data_incorrect_totp) + + def test_accses_denied_2fa_incorrect_password_correct_totp(self): + """ + An attacker is denied access to a 2fa user account when providing an incorrect password but correct totp + """ + data = TestAuthController.TEST_USER_2FA.copy() + data["password"] = "incorrect_password" + data["totp"] = None # TODO + + """ + We assume the attacker knows the username of the victim + TO TEST: + - + - + - + - An attacker is denied access to a normal user account when providing a totp + - An attacker cannot successfully submit a forged access token + - An attacker cannot successfully submit a forged refresh token + - An attacker cannot successfully reuse an expired access token + - An attacker cannot successfully reuse an expired refresh token + """