import web from views.forms import login_form from views.utils import get_nav_bar, csrf_protected, get_render, check_password, get_totp_token import models.session import models.user import logging import random import string import time logger = logging.getLogger(__name__) # The remember cookie should be valid for a week remember_timeout = 3600*24*7 # The timeout between login attempts, after the 3rd incorrect one login_timeout = 60 login_attempts_threshold = 2 class Login(): # Get the server secret to perform signatures secret = web.config.get('session_parameters')['secret_key'] def GET(self): """ Show the login page :return: The login page showing other users if logged in """ session = web.ctx.session nav = get_nav_bar(session) # Log the user in if the rememberme cookie is set and valid self.check_rememberme() return get_render().login(nav, login_form, "") @csrf_protected def POST(self): """ Log in to the web application and register the session :return: The login page showing other users if logged in """ session = web.ctx.session nav = get_nav_bar(session) data = web.input(username="", password="", remember=False) render = get_render() # Validate login credential with database query user = models.user.get_user(data.username) if user is None: return render.login(nav, login_form, "- User authentication failed") if user["login_attempts"] > login_attempts_threshold and user["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.") two_factor = get_totp_token(user["authenticator_secret"]) == data.authenticator_secret if two_factor and check_password(data.password, user["password"]): if user["login_attempts"] > login_attempts_threshold: logger.info("User %s logged in succesfully after %s attempts", user["username"], user["login_attempts"]) if not models.user.is_verified(user["userid"]): return render.login(nav, login_form, "- User not authenticated yet. Please check you email.") if user["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(user["userid"], "") models.user.set_login_attempts(user["userid"], 0, time.time()) self.login(user["username"], user["userid"], data.remember) raise web.seeother("/") elif two_factor and check_password(data.password, user["temporary_password"]): session.temporary_userid = user["userid"] raise web.seeother("/reset") else: logger.warning("Incorrect login attempt on user %s by IP %s", user["username"], web.ctx.ip) models.user.set_login_attempts(user["userid"], user["login_attempts"]+1, time.time()) if user["login_attempts"] == login_attempts_threshold: return render.login(nav, login_form, "- Too many incorrect login attempts. You have to wait a minute before trying again.") else: return render.login(nav, login_form, "- User authentication failed") def login(self, username, userid, remember): """ Log in to the application """ session = web.ctx.session session.username = username session.userid = userid if remember: rememberme = self.rememberme(remember_timeout) 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): """ Validate the rememberme cookie and log in """ 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_token = cookies.remember 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 and expiry > time.time(): username = models.user.get_user_name_by_id(userid) self.login(username, userid, False) def rememberme(self, timeout): """ Generate a random token for the user, and store it in the database. """ session = web.ctx.session alphabet = string.ascii_uppercase + string.digits while True: token = ''.join(random.SystemRandom().choice(alphabet) for _ in range(20)) if models.session.get_cookie(token)[0] is None: break models.session.set_cookie(session.userid, token, int(time.time() + timeout)) return token