Parcourir la source

Implement password reset

Fixes #2
pull/40/head
Sindre Stephansen il y a 5 ans
Parent
révision
dd27cb68a4
10 fichiers modifiés avec 230 ajouts et 16 suppressions
  1. +5
    -4
      mysql/sql/init.sql
  2. +2
    -2
      src/app/models/register.py
  3. +39
    -1
      src/app/models/user.py
  4. +1
    -0
      src/app/templates/login.html
  5. +25
    -0
      src/app/templates/reset.html
  6. +25
    -0
      src/app/templates/reset_request.html
  7. +3
    -0
      src/app/views/app.py
  8. +22
    -8
      src/app/views/forms.py
  9. +8
    -1
      src/app/views/login.py
  10. +100
    -0
      src/app/views/reset.py

+ 5
- 4
mysql/sql/init.sql Voir le fichier

@@ -3,6 +3,7 @@ CREATE TABLE users (
userid INT UNSIGNED AUTO_INCREMENT,
username VARCHAR(45) UNIQUE NOT NULL,
password VARCHAR(60) NOT NULL,
temporary_password VARCHAR(60) NOT NULL DEFAULT "",
full_name VARCHAR(200) NOT NULL,
company VARCHAR(50),
email VARCHAR(50) NOT NULL,
@@ -11,10 +12,10 @@ CREATE TABLE users (
state VARCHAR(50),
postal_code VARCHAR(50),
country VARCHAR(50),
login_attempts INT UNSIGNED,
last_login_attempt INT UNSIGNED,
verified BOOLEAN,
token VARCHAR(50),
login_attempts INT UNSIGNED DEFAULT 0,
last_login_attempt INT UNSIGNED DEFAULT 0,
verified BOOLEAN DEFAULT 0,
token VARCHAR(50) NOT NULL DEFAULT "",
PRIMARY KEY (userid)
);



+ 2
- 2
src/app/models/register.py Voir le fichier

@@ -37,8 +37,8 @@ def set_user(username, password, full_name, company, email,
query = ("""
INSERT INTO users (userid, username, password, full_name, company,
email, street_address, city, state, postal_code,
country, login_attempts, last_login_attempt, verified, token)
VALUES (NULL, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 0, 0, 0, %s)
country, token)
VALUES (NULL, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""")
try:
cursor.execute(query, (username, password, full_name, company, email,


+ 39
- 1
src/app/models/user.py Voir le fichier

@@ -15,7 +15,7 @@ def get_user(username):
"""
db.connect()
cursor = db.cursor()
query = ("SELECT userid, username, password, login_attempts, last_login_attempt from users where username = %s")
query = ("SELECT userid, username, email, password, temporary_password, login_attempts, last_login_attempt from users where username = %s")
user = None
try:
cursor.execute(query, (username,))
@@ -54,6 +54,44 @@ def get_users():
return users


def set_temporary_password(userid, password):
"""
Set a temporary password for a user
"""
db.connect()
cursor = db.cursor()
query = ("UPDATE users SET temporary_password = %s WHERE userid = %s")
try:
cursor.execute(query, (password, userid))
db.commit()
except mysql.connector.Error as err:
logger.error("Failed executing query: %s", err)
cursor.fetchall()
exit(1)
finally:
cursor.close()
db.close()


def set_password(userid, password):
"""
Set the password for a user
"""
db.connect()
cursor = db.cursor()
query = ("UPDATE users SET password = %s WHERE userid = %s")
try:
cursor.execute(query, (password, userid))
db.commit()
except mysql.connector.Error as err:
logger.error("Failed executing query: %s", err)
cursor.fetchall()
exit(1)
finally:
cursor.close()
db.close()


def set_login_attempts(userid, num, timestamp):
"""
Set the number and timestamp of the failed login attempts for the given user.


+ 1
- 0
src/app/templates/login.html Voir le fichier

@@ -17,6 +17,7 @@ $def with (nav, login_form, message)
<form method="POST">
$:csrf_field()
$:login_form.render()
<a href="/request_reset">Forgot your password?</a>
</form>

$else:


+ 25
- 0
src/app/templates/reset.html Voir le fichier

@@ -0,0 +1,25 @@
$def with (nav, reset_form, message)

<head>
<title>Beelance2</title>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="static/stylesheet.css">
<link rel="shortcut icon" type="image/png" href="static/honeybee.png"/>

</head>

<body>

$:nav

<h2>Reset password</h2>

<form method="POST">
$:csrf_field()
$:reset_form.render()
</form>

<p>$message</p>
</body>

<footer></footer>

+ 25
- 0
src/app/templates/reset_request.html Voir le fichier

@@ -0,0 +1,25 @@
$def with (nav, reset_request_form, message)

<head>
<title>Beelance2</title>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="static/stylesheet.css">
<link rel="shortcut icon" type="image/png" href="static/honeybee.png"/>

</head>

<body>

$:nav

<h2>Reset password</h2>

<form method="POST">
$:csrf_field()
$:reset_request_form.render()
</form>

<p>$message</p>
</body>

<footer></footer>

+ 3
- 0
src/app/views/app.py Voir le fichier

@@ -3,6 +3,7 @@ import web
from views.login import Login
from views.logout import Logout
from views.register import Register, Verify
from views.reset import RequestReset, Reset
from views.new_project import New_project
from views.open_projects import Open_projects
from views.project import Project
@@ -18,6 +19,8 @@ urls = (
'/login', 'Login',
'/logout', 'Logout',
'/verify', 'Verify',
'/reset', 'Reset',
'/request_reset', 'RequestReset',
'/register', 'Register',
'/new_project', 'New_project',
'/open_projects', 'Open_projects',


+ 22
- 8
src/app/views/forms.py Voir le fichier

@@ -1,5 +1,5 @@
from web import form
from models.project import get_categories
from models.project import get_categories
from models.user import get_users, get_user_id_by_name


@@ -9,7 +9,7 @@ vpass = form.regexp(r".{6,100}$", '- Must be atleast 6 characters long')
number = form.regexp(r"^[0-9]+$", "- Must be a number")
not_empty = form.regexp(r".+", "- This field is required")

# Define the login form
# Define the login form
login_form = form.Form(
form.Textbox("username", description="Username"),
form.Password("password", description="Password"),
@@ -17,7 +17,7 @@ login_form = form.Form(
form.Button("Log In", type="submit", description="Login"),
)

# Define the register form
# Define the register form
register_form = form.Form(
form.Textbox("username", not_empty, description="Username"),
form.Textbox("full_name", not_empty, description="Full name"),
@@ -32,6 +32,20 @@ register_form = form.Form(
form.Button("Register", type="submit", description="Register")
)

# Define the reset forms
request_reset_form = form.Form(
form.Textbox("username", description="Username"),
form.Textbox("email", description="Email address"),
form.Button("Submit", type="submit", description="Submit"),
)

reset_form = form.Form(
form.Password("temporary", description="Temporary password"),
form.Password("password", description="New password"),
form.Password("repeat", description="Repeat new password"),
form.Button("Change password", type="submit", description="Change password"),
)

# Define the project view form
project_form = form.Form(
form.Input("myfile", type="file"),
@@ -47,7 +61,7 @@ def get_task_form_elements(identifier=0, task_title="", task_description="", bud
Generate a set of task form elements
:param identifier: The id of the task
:param task_title: Task title
:param task_description: Task description
:param task_description: Task description
:param budget: Task budget
:type identifier: int, str
:type task_title: str
@@ -66,12 +80,12 @@ def get_project_form_elements(project_title="", project_description="", category
"""
Generate a set of project form elements
:param project_title: Project title
:param project_description: Project description
:param project_description: Project description
:param category_name: Name of the belonging category
:type project_title: str
:type project_description: str
:type category_name: str
:return: A set of project form elements
:return: A set of project form elements
"""
categories = get_categories()
project_form_elements = (
@@ -97,7 +111,7 @@ def get_user_form_elements(identifier=0, user_name="", read_permission=True, wri
:return: The form elements to add users to a project
"""
user_form_elements = (
form.Textbox("user_name_" + str(identifier), description="User", value=user_name, placeholder="Leave blank for open project"),
form.Textbox("user_name_" + str(identifier), description="User", value=user_name, placeholder="Leave blank for open project"),
form.Checkbox("read_permission_" + str(identifier), description="Read Permission", checked=read_permission, value=True),
form.Checkbox("write_permission_" + str(identifier), description="Write Permission", checked=write_permission, value=True),
form.Checkbox("modify_permission_" + str(identifier), description="Modify Permission", checked=modify_permission, value=True)
@@ -147,5 +161,5 @@ def get_apply_permissions_form(identifier=0, read_permission="TRUE", write_permi
form.Checkbox("read_permission_" + str(identifier), description="Read Permission", checked=(read_permission=="TRUE"), value=True),
form.Checkbox("write_permission_" + str(identifier), description="Write Permission", checked=(write_permission=="TRUE"), value=True),
form.Checkbox("modify_permission_" + str(identifier), description="Modify Permission", checked=(modify_permission=="TRUE"), value=True)
)
)
return user_permissions

+ 8
- 1
src/app/views/login.py Voir le fichier

@@ -55,7 +55,7 @@ class Login():
if user is None:
return render.login(nav, login_form, "- User authentication failed")

userid, username, password_hash, login_attempts, last_login_attempt = user
userid, username, _, password_hash, temporary_password, login_attempts, last_login_attempt = user

if login_attempts > login_attempts_threshold and last_login_attempt + login_timeout > time.time():
return render.login(nav, login_form, "- There have been too many incorrect login attempts for your account. You have to wait a minute before you can log in.")
@@ -67,9 +67,16 @@ class Login():
if not models.user.is_verified(userid):
return render.login(nav, login_form, "- User not authenticated yet. Please check you email.")

if temporary_password:
logger.info("A password reset was requested for user %s, but they logged in with the old password before resetting")
models.user.set_temporary_password(userid, "")

models.user.set_login_attempts(userid, 0, time.time())
self.login(username, userid, data.remember)
raise web.seeother("/")
elif temporary_password and bcrypt.checkpw(data.password.encode('UTF-8'), temporary_password.encode('UTF-8')):
session.temporary_userid = userid
raise web.seeother("/reset")
else:
logger.warning("Incorrect login attempt on user %s by IP %s", username, web.ctx.ip)
models.user.set_login_attempts(userid, login_attempts+1, time.time())


+ 100
- 0
src/app/views/reset.py Voir le fichier

@@ -0,0 +1,100 @@
import web
from uuid import uuid4
from views.forms import reset_form, request_reset_form
from views.utils import get_nav_bar, csrf_protected, get_render, password_weakness, sendmail
import models.user
import logging
import bcrypt

logger = logging.getLogger(__name__)


class RequestReset:
def GET(self):
session = web.ctx.session
nav = get_nav_bar(session)

return get_render().reset_request(nav, request_reset_form, "")

@csrf_protected
def POST(self):
session = web.ctx.session
nav = get_nav_bar(session)
data = web.input(username="", email="")
render = get_render()

user = models.user.get_user(data.username)
if user and user[2] == data.email:
password = uuid4().hex
password_hash = bcrypt.hashpw(password.encode('UTF-8'), bcrypt.gensalt())
models.user.set_temporary_password(user[0], password_hash)

sendmail(
'Reset your Beelance password',
"""
Hi!

Someone requested a password reset for your account. If that wasn't you you can ignore this email.

If you want to reset your password, log in with this password: {password}
Then you will be able to set a new password.
""".format(password=password),
"",
data.email,
)

logger.info("User %s requested a password reset", data.username)
else:
logger.info("Incorrect reset request with username %s and email %s", data.username, data.email)

return render.reset_request(nav, request_reset_form, "An email has been sent, if the username and email is correct")


class Reset:
def GET(self):
session = web.ctx.session
nav = get_nav_bar(session)
render = get_render()

if 'temporary_userid' not in session or not session.temporary_userid:
return render.reset(nav, reset_form, "Something went wrong. Try logging in with the temporary password again.")
return get_render().reset(nav, reset_form, "")

@csrf_protected
def POST(self):
session = web.ctx.session
nav = get_nav_bar(session)
data = web.input(temporary="", password="", repeat="")
render = get_render()

if 'temporary_userid' not in session or not session.temporary_userid:
return render.reset(nav, reset_form, "Something went wrong. Try logging in with the temporary password again.")

userid = session.temporary_userid
username = models.user.get_user_name_by_id(userid)
user = models.user.get_user(username)
temporary_password = user[4]

# Check that the temporary password is correct
if not bcrypt.checkpw(data.temporary.encode('UTF-8'), temporary_password.encode('UTF-8')):
return render.reset(nav, reset_form, "Incorrect temporary password")

# Check that the passwords match
if data.password != data.repeat:
return render.reset(nav, reset_form, "The repeated password doesn't match the first")

# Check password security
weakness = password_weakness(data.password, username)
if weakness is not None:
return render.reset(nav, reset_form, weakness)

# Set the new password and log the user in
password_hash = bcrypt.hashpw(data.password.encode('UTF-8'), bcrypt.gensalt())
models.user.set_password(userid, password_hash)
models.user.set_temporary_password(userid, "")

session.temporary_userid = None

logger.info("User %s has changed their password", username)

return get_render().reset(nav, reset_form, "Your password has been reset. You can log in again now.")

Chargement…
Annuler
Enregistrer