Skip to content

Commit

Permalink
Merge pull request #180 from jupyterhub/confirm-passwords
Browse files Browse the repository at this point in the history
Ask for new passwords twice, ask for current password on change
  • Loading branch information
lambdaTotoro committed Nov 2, 2021
2 parents 9e34885 + 7b56ac6 commit b38f8ec
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 52 deletions.
107 changes: 75 additions & 32 deletions nativeauthenticator/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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)),
}
Expand All @@ -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:
Expand Down Expand Up @@ -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(
Expand All @@ -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)

Expand Down Expand Up @@ -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)

Expand All @@ -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)

Expand Down
17 changes: 10 additions & 7 deletions nativeauthenticator/nativeauthenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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.",
)

Expand All @@ -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

Expand Down
58 changes: 58 additions & 0 deletions nativeauthenticator/templates/change-password-admin.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{% extends "page.html" %}

{% block script %}
{{ super() }}
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', function() {
let button = document.getElementById('eye');
button.addEventListener("click", function(e) {
let npwd = document.getElementById("new_password_input");
let cpwd = document.getElementById("new_password_confirmation_input");
if (npwd.getAttribute("type") === "password") {
npwd.setAttribute("type", "text");
cpwd.setAttribute("type", "text");
button.textContent = "🔑";
} else {
npwd.setAttribute("type", "password");
cpwd.setAttribute("type", "password");
button.textContent = "👁";
}
});
});
</script>
{% endblock script %}

{% block main %}
<div class="container">
<form action="{{post_url}}" method="post" role="form">
<h1>
Change password for {{user_name}}
</h1>

<p>Please enter the new password you want to set for {{user_name}}.</p>

<div class="form-group">
<label for="new_password_input">New password:</label>
<div class="input-group col-xs-6">
<input id="new_password_input" type="password" name="new_password" val="" autocapitalize="off" autocorrect="off" class="form-control" />
<span class="input-group-addon">
<button id="eye" type="button" style="border:0;">👁</button>
</span>
</div>
<p></p>

<label for="new_password_confirmation_input">Confirm new password:</label>
<div class="input-group col-xs-6">
<input id="new_password_confirmation_input" type="password" name="new_password_confirmation" val="" autocapitalize="off" autocorrect="off" class="form-control" />
</div>
<p></p>

<input id="signup_submit" type="submit" value='Change Password' class='btn btn-jupyter' />
</div>
</form>

{% if result_message %}
<div class="alert {{alert}} col-xs-6" role="alert">{{result_message}}</div>
{% endif %}
</div>
{% endblock %}
37 changes: 29 additions & 8 deletions nativeauthenticator/templates/change-password.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "👁";
}
});
Expand All @@ -25,23 +31,38 @@
<h1>
Change password for {{user_name}}
</h1>

<p>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.</p>

<div class="form-group">
<label for="password_input">New password:</label>
<div class="input-group">
<input id="password_input" type="password" name="password" val="" autocapitalize="off" autocorrect="off" class="form-control" />
<label for="old_password_input">Old password:</label>
<div class="input-group col-xs-6">
<input id="old_password_input" type="password" name="old_password" val="" autocapitalize="off" autocorrect="off" class="form-control" />
<span class="input-group-addon">
<button id="eye" type="button" style="border:0;">👁</button>
</span>
</div>
<p></p>

<label for="new_password_input">New password:</label>
<div class="input-group col-xs-6">
<input id="new_password_input" type="password" name="new_password" val="" autocapitalize="off" autocorrect="off" class="form-control" />
</div>
<p></p>

<label for="new_password_confirmation_input">Confirm new password:</label>
<div class="input-group col-xs-6">
<input id="new_password_confirmation_input" type="password" name="new_password_confirmation" val="" autocapitalize="off" autocorrect="off" class="form-control" />
<span></span>
</div>
<p></p>

<input id="signup_submit" type="submit" value='Change Password' class='btn btn-jupyter' />
</div>
</form>

{% if result_message %}
<div class="alert {{alert}}" role="alert">{{result_message}}</div>
<div class="alert {{alert}} col-xs-6" role="alert">{{result_message}}</div>
{% endif %}
</div>
{% endblock %}
12 changes: 11 additions & 1 deletion nativeauthenticator/templates/signup.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "👁";
}
});
Expand Down Expand Up @@ -80,12 +84,18 @@

<label for="password_input">Password:</label>
<div class="input-group">
<input id="password_input" type="password" name="pw" val="" autocapitalize="off" autocorrect="off" class="form-control" />
<input id="password_input" type="password" name="signup_password" val="" autocapitalize="off" autocorrect="off" class="form-control" />
<span class="input-group-addon">
<button id="eye" type="button" style="border:0;">👁</button>
</span>
</div>
<p></p>

<label for="password_confirm">Confirm password:</label>
<div class="input-group">
<input id="password_confirmation_input" type="password" name="signup_password_confirmation" val="" autocapitalize="off" autocorrect="off" class="form-control" />
</div>
<p></p>

{% if two_factor_auth %}
<input type="checkbox" id="2fa" name="2fa" value="2fa">
Expand Down

0 comments on commit b38f8ec

Please sign in to comment.