diff --git a/server/fishtest/userdb.py b/server/fishtest/userdb.py index 6c2957b7f1..380ca7ab26 100644 --- a/server/fishtest/userdb.py +++ b/server/fishtest/userdb.py @@ -2,7 +2,15 @@ import threading import time from datetime import datetime, timezone - +from functools import lru_cache + +from argon2 import PasswordHasher +from argon2.exceptions import ( + HashingError, + InvalidHash, + VerificationError, + VerifyMismatchError, +) from fishtest.schemas import user_schema from pymongo import ASCENDING from vtjson import ValidationError, validate @@ -47,16 +55,44 @@ def clear_cache(self): with self.user_lock: self.cache.clear() + def hash_password( + self, + password, + time_cost: int = 3, + memory_cost: int = 12288, + parallelism: int = 1, + ): + return PasswordHasher(time_cost, memory_cost, parallelism).hash(password) + + @lru_cache(maxsize=128) + def check_password(self, hashed_password, password): + try: + return PasswordHasher().verify(hashed_password, password) + except InvalidHash as e: + print("InvalidHash:", e, sep="\n") + except VerifyMismatchError as e: + print("VerifyMismatchError:", e, sep="\n") + except HashingError as e: + print("HashingError:", e, sep="\n") + except VerificationError as e: + print("VerificationError:", e, sep="\n") + except Exception as e: + print("Exception:", e, sep="\n") + return False + def authenticate(self, username, password): user = self.get_user(username) - if not user or user["password"] != password: - sys.stderr.write("Invalid login: '{}' '{}'\n".format(username, password)) + if not user: + sys.stderr.write("Invalid username: '{}'\n".format(username)) + return {"error": "Invalid username: {}".format(username)} + if not self.check_password(user["password"], password): + sys.stderr.write("Invalid login: '{}'\n".format(username)) return {"error": "Invalid password for user: {}".format(username)} if "blocked" in user and user["blocked"]: - sys.stderr.write("Blocked account: '{}' '{}'\n".format(username, password)) + sys.stderr.write("Blocked account: '{}'\n".format(username)) return {"error": "Account blocked for user: {}".format(username)} if "pending" in user and user["pending"]: - sys.stderr.write("Pending account: '{}' '{}'\n".format(username, password)) + sys.stderr.write("Pending account: '{}'\n".format(username)) return {"error": "Account pending for user: {}".format(username)} return {"username": username, "authenticated": True} diff --git a/server/fishtest/views.py b/server/fishtest/views.py index e560520c21..d3659f01ca 100644 --- a/server/fishtest/views.py +++ b/server/fishtest/views.py @@ -427,7 +427,7 @@ def signup(request): result = request.userdb.create_user( username=signup_username, - password=signup_password, + password=request.userdb.hash_password(signup_password), email=validated_email, tests_repo=tests_repo, ) @@ -621,8 +621,7 @@ def user(request): new_email = request.params.get("email").strip() tests_repo = request.params.get("tests_repo").strip() - # Temporary comparison until passwords are hashed. - if old_password != user_data["password"].strip(): + if not request.userdb.check_password(user["password"], old_password): request.session.flash("Invalid password!", "error") return home(request) @@ -635,7 +634,9 @@ def user(request): (new_email if len(new_email) > 0 else None), ) if strong_password: - user_data["password"] = new_password + user_data["password"] = request.userdb.hash_password( + new_password + ) request.session.flash("Success! Password updated") else: request.session.flash( diff --git a/server/setup.py b/server/setup.py index 454a82de75..e2134df503 100644 --- a/server/setup.py +++ b/server/setup.py @@ -15,6 +15,7 @@ "awscli", "zxcvbn", "email_validator", + "argon2-cffi", "vtjson", ] diff --git a/server/tests/test_api.py b/server/tests/test_api.py index fb017fb2fd..c51f65188a 100644 --- a/server/tests/test_api.py +++ b/server/tests/test_api.py @@ -138,7 +138,7 @@ def setUpClass(cls): } cls.rundb.userdb.create_user( cls.username, - cls.password, + "$argon2id$v=19$m=12288,t=3,p=1$CBZVlx94y2hWHPm1D6Vo3A$19cJ5J4prNe6aObbgFBHpwzyeg1DwiuWHWXG6srMq7w", "email@email.email", "https://github.com/official-stockfish/Stockfish", ) @@ -526,7 +526,7 @@ def setUpClass(cls): } cls.rundb.userdb.create_user( cls.username, - cls.password, + "$argon2id$v=19$m=12288,t=3,p=1$CBZVlx94y2hWHPm1D6Vo3A$19cJ5J4prNe6aObbgFBHpwzyeg1DwiuWHWXG6srMq7w", "email@email.email", "https://github.com/official-stockfish/Stockfish", ) diff --git a/server/tests/test_users.py b/server/tests/test_users.py index 663bdffdd8..2dcddf403a 100644 --- a/server/tests/test_users.py +++ b/server/tests/test_users.py @@ -41,7 +41,7 @@ def setUp(self): self.rundb = util.get_rundb() self.rundb.userdb.create_user( "JoeUser", - "secret", + "$argon2id$v=19$m=12288,t=3,p=1$CBZVlx94y2hWHPm1D6Vo3A$19cJ5J4prNe6aObbgFBHpwzyeg1DwiuWHWXG6srMq7w", "email@email.email", "https://github.com/official-stockfish/Stockfish", ) @@ -55,6 +55,7 @@ def tearDown(self): testing.tearDown() def test_login(self): + # Pending user, wrong password request = testing.DummyRequest( userdb=self.rundb.userdb, method="POST", @@ -65,21 +66,51 @@ def test_login(self): "Invalid password for user: JoeUser" in request.session.pop_flash("error") ) - # Correct password, but still pending from logging in + # Pending user, correct password request.params["password"] = "secret" login(request) self.assertTrue( "Account pending for user: JoeUser" in request.session.pop_flash("error")[0] ) - # Unblock, then user can log in successfully + # Approved user, wrong password user = self.rundb.userdb.get_user("JoeUser") user["pending"] = False self.rundb.userdb.save_user(user) + request.params["password"] = "badsecret" + response = login(request) + self.assertTrue( + "Invalid password for user: JoeUser" in request.session.pop_flash("error") + ) + + # Approved user, correct password + request.params["password"] = "secret" + response = login(request) + self.assertEqual(response.code, 302) + self.assertTrue("The resource was found at" in str(response)) + + # User is blocked, correct password + user["blocked"] = True + self.rundb.userdb.save_user(user) + response = login(request) + self.assertTrue( + "Account blocked for user: JoeUser" in request.session.pop_flash("error")[0] + ) + + # User is unblocked, correct password + user["blocked"] = False + self.rundb.userdb.save_user(user) response = login(request) self.assertEqual(response.code, 302) self.assertTrue("The resource was found at" in str(response)) + # Invalid username, correct password + request.params["username"] = "UserJoe" + response = login(request) + self.assertTrue( + "Invalid username: UserJoe" in request.session.pop_flash("error")[0] + ) + class Create90APITest(unittest.TestCase): def setUp(self):