From ba8b2e615344020a304ba03c58a69c4407cfa601 Mon Sep 17 00:00:00 2001 From: Sindre Stephansen Date: Mon, 9 Mar 2020 14:50:29 +0100 Subject: [PATCH 1/4] Use random string for remember cookie, replacing deserialization The random string token is stored in the database, and is revoked when the user logs out. Fixes #17 --- mysql/sql/init.sql | 10 ++++-- src/app/models/session.py | 71 +++++++++++++++++++++++++++++++++++++++ src/app/views/login.py | 46 +++++++++++-------------- src/app/views/logout.py | 6 +++- 4 files changed, 103 insertions(+), 30 deletions(-) create mode 100644 src/app/models/session.py diff --git a/mysql/sql/init.sql b/mysql/sql/init.sql index 3251df7..21936a5 100644 --- a/mysql/sql/init.sql +++ b/mysql/sql/init.sql @@ -14,6 +14,13 @@ CREATE TABLE users ( PRIMARY KEY (userid) ); +CREATE TABLE cookies ( + token VARCHAR(100) UNIQUE NOT NULL, + userid INT UNSIGNED NOT NULL, + PRIMARY KEY (token), + FOREIGN KEY (userid) REFERENCES users(userid) +); + CREATE TABLE project_category ( categoryid INT UNSIGNED AUTO_INCREMENT, category_name VARCHAR(200) UNIQUE NOT NULL, @@ -84,9 +91,8 @@ insert into project_category values (NULL, "Grocery shopping"); /* -Create default database user +Create default database user */ CREATE USER 'root'@'10.5.0.6' IDENTIFIED BY 'root'; GRANT ALL PRIVILEGES ON db.* TO 'root'@'10.5.0.6'; - diff --git a/src/app/models/session.py b/src/app/models/session.py new file mode 100644 index 0000000..12345a9 --- /dev/null +++ b/src/app/models/session.py @@ -0,0 +1,71 @@ +from models.database import db +import mysql.connector + + +def set_cookie(userid, token): + """ + Register a persistant login token for an user + :param userid: The ID of the user + :param token: A random unique token for the cookie + """ + db.connect() + cursor = db.cursor() + query = ("INSERT INTO cookies (userid, token) VALUES (%s, %s)") + + try: + cursor.execute(query, (userid, token)) + db.commit() + except mysql.connector.Error as err: + print("Failed executing query: {}".format(err)) + cursor.fetchall() + exit(1) + finally: + cursor.close() + db.close() + + +def get_cookie(token): + """ + Get the userid of the user with the given persistant login token + :param token: The token to search for + """ + db.connect() + cursor = db.cursor() + query = ("SELECT userid FROM cookies WHERE token = %s") + userid = None + + try: + cursor.execute(query, (token,)) + users = cursor.fetchall() + if len(users): + userid = users[0][0] + except mysql.connector.Error as err: + print("Failed executing query: {}".format(err)) + cursor.fetchall() + exit(1) + finally: + cursor.close() + db.close() + + return userid + + +def delete_cookie(token): + """ + Get the userid of the user with the given persistant login token + :param token: The token to delete + """ + db.connect() + cursor = db.cursor() + query = ("DELETE FROM cookies WHERE token = %s") + + try: + cursor.execute(query, (token,)) + db.commit() + except mysql.connector.Error as err: + print("Failed executing query: {}".format(err)) + cursor.fetchall() + exit(1) + finally: + cursor.close() + db.close() diff --git a/src/app/views/login.py b/src/app/views/login.py index 5a7adde..15fb410 100644 --- a/src/app/views/login.py +++ b/src/app/views/login.py @@ -1,8 +1,10 @@ import web from views.forms import login_form +import models.session import models.user from views.utils import get_nav_bar -import os, hmac, base64, pickle +import random +import string import hashlib # Get html templates @@ -17,7 +19,7 @@ class Login(): def GET(self): """ Show the login page - + :return: The login page showing other users if logged in """ session = web.ctx.session @@ -40,7 +42,7 @@ class Login(): # Validate login credential with database query password_hash = hashlib.md5(b'TDT4237' + data.password.encode('utf-8')).hexdigest() user = models.user.match_user(data.username, password_hash) - + # If there is a matching user/password in the database the user is logged in if user: self.login(user[1], user[0], data.remember) @@ -63,44 +65,34 @@ class Login(): """ Validate the rememberme cookie and log in """ - username = "" - sign = "" + userid = None # If the user selected 'remember me' they log in automatically try: # Fetch the users cookies if it exists cookies = web.cookies() # Fetch the remember cookie and convert from string to bytes - remember_hash = bytes(cookies.remember[2:][:-1], 'ascii') - # Decode the hash - decode = base64.b64decode(remember_hash) - # Load the decoded hash to receive the host signature and the username - username, sign = pickle.loads(decode) + remember_token = cookies.remember + userid = models.session.get_cookie(remember_token) except AttributeError as e: # The user did not have the stored remember me cookie pass # If the users signed cookie matches the host signature then log in - if self.sign_username(username) == sign: - userid = models.user.get_user_id_by_name(username) + if userid is not None: + username = models.user.get_user_name_by_id(userid) self.login(username, userid, False) def rememberme(self): """ - Encode a base64 object consisting of the username signed with the - host secret key and the username. Can be reassembled with the - hosts secret key to validate user. - :return: base64 object consisting of signed username and username + Generate a random token for the user, and store it in the database. """ session = web.ctx.session - creds = [ session.username, self.sign_username(session.username) ] - return base64.b64encode(pickle.dumps(creds)) + alphabet = string.ascii_uppercase + string.digits - @classmethod - def sign_username(self, username): - """ - Sign the current users name with the hosts secret key - :return: The users signed name - """ - secret = base64.b64decode(self.secret) - return hmac.HMAC(secret, username.encode('ascii')).hexdigest() - \ No newline at end of file + while True: + token = ''.join(random.SystemRandom().choice(alphabet) for _ in range(20)) + if models.session.get_cookie(token) is None: + break + + models.session.set_cookie(session.userid, token) + return token diff --git a/src/app/views/logout.py b/src/app/views/logout.py index a24bda9..839a1c4 100644 --- a/src/app/views/logout.py +++ b/src/app/views/logout.py @@ -1,5 +1,6 @@ import web from views.utils import get_nav_bar +import models.session # Get html templates render = web.template.render('templates/') @@ -12,9 +13,12 @@ class Logout: Log out of the application (kill session and reset variables) :return: Redirect to main page """ + remember_token = web.cookies().remember + models.session.delete_cookie(remember_token) + session = web.ctx.session session.username = None session.userid = None web.setcookie('remember', '', 0) - session.kill() + session.kill() raise web.seeother('/') From 56c14f149f273bdd965abd603e1310c350aaa0dc Mon Sep 17 00:00:00 2001 From: Sindre Stephansen Date: Mon, 9 Mar 2020 15:39:29 +0100 Subject: [PATCH 2/4] Reduce remember cookie expiry, and enforce by storing it in the database Fixes #14 --- mysql/sql/init.sql | 1 + src/app/models/session.py | 12 +++++++----- src/app/views/login.py | 20 ++++++++++++-------- src/app/views/logout.py | 8 ++++++-- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/mysql/sql/init.sql b/mysql/sql/init.sql index 21936a5..165cbe6 100644 --- a/mysql/sql/init.sql +++ b/mysql/sql/init.sql @@ -17,6 +17,7 @@ CREATE TABLE users ( CREATE TABLE cookies ( token VARCHAR(100) UNIQUE NOT NULL, userid INT UNSIGNED NOT NULL, + expiry INT NOT NULL, PRIMARY KEY (token), FOREIGN KEY (userid) REFERENCES users(userid) ); diff --git a/src/app/models/session.py b/src/app/models/session.py index 12345a9..eb65e25 100644 --- a/src/app/models/session.py +++ b/src/app/models/session.py @@ -2,7 +2,7 @@ from models.database import db import mysql.connector -def set_cookie(userid, token): +def set_cookie(userid, token, expiry): """ Register a persistant login token for an user :param userid: The ID of the user @@ -10,10 +10,10 @@ def set_cookie(userid, token): """ db.connect() cursor = db.cursor() - query = ("INSERT INTO cookies (userid, token) VALUES (%s, %s)") + query = ("INSERT INTO cookies (userid, token, expiry) VALUES (%s, %s, %s)") try: - cursor.execute(query, (userid, token)) + cursor.execute(query, (userid, token, expiry)) db.commit() except mysql.connector.Error as err: print("Failed executing query: {}".format(err)) @@ -31,14 +31,16 @@ def get_cookie(token): """ db.connect() cursor = db.cursor() - query = ("SELECT userid FROM cookies WHERE token = %s") + query = ("SELECT userid, expiry FROM cookies WHERE token = %s") userid = None + expiry = None try: cursor.execute(query, (token,)) users = cursor.fetchall() if len(users): userid = users[0][0] + expiry = users[0][1] except mysql.connector.Error as err: print("Failed executing query: {}".format(err)) cursor.fetchall() @@ -47,7 +49,7 @@ def get_cookie(token): cursor.close() db.close() - return userid + return userid, expiry def delete_cookie(token): diff --git a/src/app/views/login.py b/src/app/views/login.py index 15fb410..6866d37 100644 --- a/src/app/views/login.py +++ b/src/app/views/login.py @@ -6,10 +6,14 @@ from views.utils import get_nav_bar import random import string import hashlib +import time # Get html templates render = web.template.render('templates/') +# The remember cookie should be valid for a week +remember_timeout = 3600*24*7 + class Login(): @@ -58,8 +62,8 @@ class Login(): session.username = username session.userid = userid if remember: - rememberme = self.rememberme() - web.setcookie('remember', rememberme , 300000000) + rememberme = self.rememberme(remember_timeout) + web.setcookie('remember', rememberme , remember_timeout) def check_rememberme(self): """ @@ -72,17 +76,17 @@ class Login(): cookies = web.cookies() # Fetch the remember cookie and convert from string to bytes remember_token = cookies.remember - userid = models.session.get_cookie(remember_token) - except AttributeError as e: + userid, expiry = models.session.get_cookie(remember_token) + except AttributeError: # The user did not have the stored remember me cookie pass # If the users signed cookie matches the host signature then log in - if userid is not None: + if userid is not None and expiry > time.time(): username = models.user.get_user_name_by_id(userid) self.login(username, userid, False) - def rememberme(self): + def rememberme(self, timeout): """ Generate a random token for the user, and store it in the database. """ @@ -91,8 +95,8 @@ class Login(): while True: token = ''.join(random.SystemRandom().choice(alphabet) for _ in range(20)) - if models.session.get_cookie(token) is None: + if models.session.get_cookie(token)[0] is None: break - models.session.set_cookie(session.userid, token) + models.session.set_cookie(session.userid, token, int(time.time() + timeout)) return token diff --git a/src/app/views/logout.py b/src/app/views/logout.py index 839a1c4..56fafc3 100644 --- a/src/app/views/logout.py +++ b/src/app/views/logout.py @@ -13,8 +13,12 @@ class Logout: Log out of the application (kill session and reset variables) :return: Redirect to main page """ - remember_token = web.cookies().remember - models.session.delete_cookie(remember_token) + try: + remember_token = web.cookies().remember + models.session.delete_cookie(remember_token) + except AttributeError: + # The user did not have the stored remember me cookie + pass session = web.ctx.session session.username = None From 1257cadf7093b1848962900b8270e257dbc55ea1 Mon Sep 17 00:00:00 2001 From: Sindre Stephansen Date: Mon, 9 Mar 2020 15:47:07 +0100 Subject: [PATCH 3/4] Secure remember cookie. This doesn't enable http-only --- src/app/views/login.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/views/login.py b/src/app/views/login.py index 6866d37..53e2952 100644 --- a/src/app/views/login.py +++ b/src/app/views/login.py @@ -63,7 +63,7 @@ class Login(): session.userid = userid if remember: rememberme = self.rememberme(remember_timeout) - web.setcookie('remember', rememberme , remember_timeout) + web.setcookie('remember', rememberme , remember_timeout, secure=True, samesite='Strict') def check_rememberme(self): """ From dafe82af0a477a93708267b7ba6aa14957c1e738 Mon Sep 17 00:00:00 2001 From: Sindre Stephansen Date: Mon, 9 Mar 2020 16:11:00 +0100 Subject: [PATCH 4/4] Make remember cookie HttpOnly Fixes #24 --- src/app/views/login.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/views/login.py b/src/app/views/login.py index 53e2952..a7a1b70 100644 --- a/src/app/views/login.py +++ b/src/app/views/login.py @@ -63,7 +63,8 @@ class Login(): session.userid = userid if remember: rememberme = self.rememberme(remember_timeout) - web.setcookie('remember', rememberme , remember_timeout, secure=True, samesite='Strict') + path = web.ctx.homepath + "/" + web.ctx.headers.append(('Set-Cookie', f'remember={rememberme}; Max-Age={remember_timeout}; Path={path}; Secure; HttpOnly; SameSite=Strict')) def check_rememberme(self): """