diff --git a/nativeauthenticator/handlers.py b/nativeauthenticator/handlers.py index 4f584ce..b799ef5 100644 --- a/nativeauthenticator/handlers.py +++ b/nativeauthenticator/handlers.py @@ -58,7 +58,7 @@ async def get(self): ) self.finish(html) - def get_result_message(self, user, taken, human=True): + def get_result_message(self, user, taken, confirmation_matches, human=True): alert = "alert-info" message = "Your information has been sent to the admin" if user and user.login_email_sent: @@ -72,6 +72,9 @@ def get_result_message(self, user, taken, human=True): "username is already in use. Please try again " "with a different username." ) + elif not confirmation_matches: + alert = "alert-danger" + message = "Your password did not match the confirmation. Please try again." else: # Error if user creation was not successful. if not user: @@ -133,7 +136,7 @@ async def post(self): if assume_human: user_info = { "username": self.get_body_argument("username", strip=False), - "pw": self.get_body_argument("pw", strip=False), + "password": self.get_body_argument("signup_password", strip=False), "email": self.get_body_argument("email", "", strip=False), "has_2fa": bool(self.get_body_argument("2fa", "", strip=False)), } @@ -143,7 +146,15 @@ async def post(self): user = False taken = False - alert, message = self.get_result_message(user, taken, assume_human) + password = self.get_body_argument("signup_password", strip=False) + confirmation = self.get_body_argument( + "signup_password_confirmation", strip=False + ) + confirmation_matches = password == confirmation + + alert, message = self.get_result_message( + user, taken, confirmation_matches, assume_human + ) otp_secret, user_2fa = "", "" if user: @@ -187,7 +198,7 @@ async def get(self, slug): class AuthorizeHandler(LocalBase): async def get(self, slug): must_stop = True - msg = "Invalid URL" + message = "Invalid URL" if self.authenticator.allow_self_approval_for: try: data = AuthorizeHandler.validate_slug( @@ -199,17 +210,17 @@ async def get(self, slug): if not must_stop: username = data["username"] - msg = f"{username} was already authorized" + message = f"{username} was already authorized" usr = UserInfo.find(self.db, username) if not usr.is_authorized: UserInfo.change_authorization(self.db, username) - msg = f"{username} has been authorized" + message = f"{username} has been authorized" # add POSIX user!! html = await self.render_template( "my_message.html", - message=msg, + message=message, ) self.finish(html) @@ -266,23 +277,43 @@ async def get(self): @web.authenticated async def post(self): user = await self.get_current_user() - new_password = self.get_body_argument("password", strip=False) - success = self.authenticator.change_password(user.name, new_password) + old_password = self.get_body_argument("old_password", strip=False) + new_password = self.get_body_argument("new_password", strip=False) + confirmation = self.get_body_argument("new_password_confirmation", strip=False) - if success: - alert = "alert-success" - msg = "Your password has been changed successfully!" - else: + correct_password_provided = self.authenticator.get_user( + user.name + ).is_valid_password(old_password) + + new_password_matches_confirmation = new_password == confirmation + + if not correct_password_provided: alert = "alert-danger" - pw_len = self.authenticator.minimum_password_length - msg = ( - "Something went wrong! Be sure your new " - f"password has at least {pw_len} characters and is " - "not too common." + message = "Your current password was incorrect. Please try again." + elif not new_password_matches_confirmation: + alert = "alert-danger" + message = ( + "Your new password didn't match the confirmation. Please try again." ) + else: + success = self.authenticator.change_password(user.name, new_password) + if success: + alert = "alert-success" + message = "Your password has been changed successfully!" + else: + alert = "alert-danger" + pw_len = self.authenticator.minimum_password_length + message = ( + "Something went wrong! Be sure your new " + f"password has at least {pw_len} characters and is " + "not too common." + ) html = await self.render_template( - "change-password.html", user_name=user.name, result_message=msg, alert=alert + "change-password.html", + user_name=user.name, + result_message=message, + alert=alert, ) self.finish(html) @@ -295,30 +326,42 @@ async def get(self, user_name): if not self.authenticator.user_exists(user_name): raise web.HTTPError(404) html = await self.render_template( - "change-password.html", + "change-password-admin.html", user_name=user_name, ) self.finish(html) @admin_users_scope async def post(self, user_name): - new_password = self.get_body_argument("password", strip=False) - success = self.authenticator.change_password(user_name, new_password) + new_password = self.get_body_argument("new_password", strip=False) + confirmation = self.get_body_argument("new_password_confirmation", strip=False) - if success: - alert = "alert-success" - msg = f"The password for {user_name} has been changed successfully" - else: + new_password_matches_confirmation = new_password == confirmation + + if not new_password_matches_confirmation: alert = "alert-danger" - pw_len = self.authenticator.minimum_password_length - msg = ( - "Something went wrong! Be sure the new password " - f"for {user_name} has at least {pw_len} characters and is " - "not too common." + message = ( + "The new password didn't match the confirmation. Please try again." ) + else: + success = self.authenticator.change_password(user_name, new_password) + if success: + alert = "alert-success" + message = f"The password for {user_name} has been changed successfully" + else: + alert = "alert-danger" + pw_len = self.authenticator.minimum_password_length + message = ( + "Something went wrong! Be sure the new password " + f"for {user_name} has at least {pw_len} characters and is " + "not too common." + ) html = await self.render_template( - "change-password.html", user_name=user_name, result_message=msg, alert=alert + "change-password-admin.html", + user_name=user_name, + result_message=message, + alert=alert, ) self.finish(html) diff --git a/nativeauthenticator/nativeauthenticator.py b/nativeauthenticator/nativeauthenticator.py index bd07637..93ad901 100644 --- a/nativeauthenticator/nativeauthenticator.py +++ b/nativeauthenticator/nativeauthenticator.py @@ -295,20 +295,20 @@ def get_authed_users(self): def user_exists(self, username): return self.get_user(username) is not None - def create_user(self, username, pw, **kwargs): + def create_user(self, username, password, **kwargs): username = self.normalize_username(username) - if self.user_exists(username): + if self.user_exists(username) or not self.validate_username(username): return - if not self.is_password_strong(pw) or not self.validate_username(username): + if not self.is_password_strong(password): return if not self.enable_signup: return - encoded_pw = bcrypt.hashpw(pw.encode(), bcrypt.gensalt()) - infos = {"username": username, "password": encoded_pw} + encoded_password = bcrypt.hashpw(password.encode(), bcrypt.gensalt()) + infos = {"username": username, "password": encoded_password} infos.update(kwargs) if self.open_signup or username in self.get_authed_users(): @@ -358,7 +358,7 @@ def send_approval_email(self, dest, url): raise web.HTTPError( 503, reason="Self-authorization email could not " - + "be sent. Please contact the jupyterhub " + + "be sent. Please contact the JupyterHub " + "admin about this.", ) @@ -375,7 +375,10 @@ def get_unauthed_amount(self): def change_password(self, username, new_password): user = self.get_user(username) - criteria = [user is not None, self.is_password_strong(new_password)] + criteria = [ + user is not None, + self.is_password_strong(new_password), + ] if not all(criteria): return diff --git a/nativeauthenticator/templates/change-password-admin.html b/nativeauthenticator/templates/change-password-admin.html new file mode 100644 index 0000000..977f4ba --- /dev/null +++ b/nativeauthenticator/templates/change-password-admin.html @@ -0,0 +1,58 @@ +{% extends "page.html" %} + +{% block script %} +{{ super() }} + +{% endblock script %} + +{% block main %} +
+
+

+ Change password for {{user_name}} +

+ +

Please enter the new password you want to set for {{user_name}}.

+ +
+ +
+ + + + +
+

+ + +
+ +
+

+ + +
+
+ + {% if result_message %} + + {% endif %} +
+{% endblock %} diff --git a/nativeauthenticator/templates/change-password.html b/nativeauthenticator/templates/change-password.html index cedda58..dcd02e1 100644 --- a/nativeauthenticator/templates/change-password.html +++ b/nativeauthenticator/templates/change-password.html @@ -6,12 +6,18 @@ document.addEventListener('DOMContentLoaded', function() { let button = document.getElementById('eye'); button.addEventListener("click", function(e) { - let pwd = document.getElementById("password_input"); - if (pwd.getAttribute("type") === "password") { - pwd.setAttribute("type", "text"); + let opwd = document.getElementById("old_password_input"); + let npwd = document.getElementById("new_password_input"); + let cpwd = document.getElementById("new_password_confirmation_input"); + if (opwd.getAttribute("type") === "password") { + opwd.setAttribute("type", "text"); + npwd.setAttribute("type", "text"); + cpwd.setAttribute("type", "text"); button.textContent = "🔑"; } else { - pwd.setAttribute("type", "password"); + opwd.setAttribute("type", "password"); + npwd.setAttribute("type", "password"); + cpwd.setAttribute("type", "password"); button.textContent = "👁"; } }); @@ -25,23 +31,38 @@

Change password for {{user_name}}

+ +

Please enter your current password and the new password you want to set it to. If you have forgotten your password, an admin can reset it for you.

- -
- + +
+

+ + +
+ +
+

+ + +
+ + +
+

{% if result_message %} - + {% endif %}
{% endblock %} diff --git a/nativeauthenticator/templates/signup.html b/nativeauthenticator/templates/signup.html index 8435493..2db3b7a 100644 --- a/nativeauthenticator/templates/signup.html +++ b/nativeauthenticator/templates/signup.html @@ -7,11 +7,15 @@ let button = document.getElementById('eye'); button.addEventListener("click", function(e) { let pwd = document.getElementById("password_input"); + let pwdc = document.getElementById("password_confirmation_input"); + if (pwd.getAttribute("type") === "password") { pwd.setAttribute("type", "text"); + pwdc.setAttribute("type", "text"); button.textContent = "🔑"; } else { pwd.setAttribute("type", "password"); + pwdc.setAttribute("type", "password"); button.textContent = "👁"; } }); @@ -80,12 +84,18 @@
- +

+ + +
+ +
+

{% if two_factor_auth %} diff --git a/nativeauthenticator/tests/test_authenticator.py b/nativeauthenticator/tests/test_authenticator.py index 87de477..337057f 100644 --- a/nativeauthenticator/tests/test_authenticator.py +++ b/nativeauthenticator/tests/test_authenticator.py @@ -169,9 +169,9 @@ async def test_no_change_to_bad_password(tmpcwd, app): assert auth.get_user("johnsnow").is_valid_password("ironwood") # CAN change password to something fulfilling criteria. - assert auth.change_password("johnsnow", "DaenerysTargaryen") is not None + assert auth.change_password("johnsnow", "Daenerys") is not None assert not auth.get_user("johnsnow").is_valid_password("ironwood") - assert auth.get_user("johnsnow").is_valid_password("DaenerysTargaryen") + assert auth.get_user("johnsnow").is_valid_password("Daenerys") @pytest.mark.parametrize( @@ -348,7 +348,7 @@ async def test_import_from_firstuse_invalid_password(user, pwd, tmpcwd, app): async def test_secret_key(app): auth = NativeAuthenticator(db=app.db) auth.ask_email_on_signup = False - auth.allow_self_approval_for = ".*@some-domain.com$" + auth.allow_self_approval_for = ".*@example.com$" auth.secret_key = "short" with pytest.raises(ValueError): @@ -362,7 +362,7 @@ async def test_secret_key(app): async def test_approval_url(app): auth = NativeAuthenticator(db=app.db) - auth.allow_self_approval_for = ".*@some-domain.com$" + auth.allow_self_approval_for = ".*@example.com$" auth.secret_key = "very long and kind-of random asdgaisgfjbafksdgasg" auth.setup_self_approval()