def assertParticipationInDb(self, user_id, contest_id, password, delay_time=0, extra_time=0, hidden=False, unrestricted=False, ip=None, team_code=None): """Assert that the participation with the given data is in the DB.""" db_participations = self.session.query(Participation)\ .filter(Participation.user_id == user_id)\ .filter(Participation.contest_id == contest_id).all() self.assertEqual(len(db_participations), 1) p = db_participations[0] self.assertTrue(validate_password(p.password, password)) self.assertEqual(p.hidden, hidden) self.assertEqual(p.unrestricted, unrestricted) if ip is None: self.assertIsNone(p.ip) else: self.assertCountEqual(p.ip, ip) if team_code is None: self.assertIsNone(p.team) else: self.assertEqual(p.team.code, team_code)
def post(self): username = self.get_argument("username", "") password = self.get_argument("password", "") next_page = self.get_argument("next", "/") user = self.sql_session.query(User)\ .filter(User.username == username)\ .first() participation = self.sql_session.query(Participation)\ .filter(Participation.contest == self.contest)\ .filter(Participation.user == user)\ .first() if user is None: # TODO: notify the user that they don't exist self.redirect("/?login_error=true") return if participation is None: # TODO: notify the user that they're uninvited self.redirect("/?login_error=true") return # If a contest-specific password is defined, use that. If it's # not, use the user's main password. if participation.password is None: correct_password = user.password else: correct_password = participation.password filtered_user = filter_ascii(username) filtered_pass = filter_ascii(password) if not validate_password(correct_password, password): logger.info("Login error: user=%s pass=%s remote_ip=%s." % (filtered_user, filtered_pass, self.request.remote_ip)) self.redirect("/?login_error=true") return if self.contest.ip_restriction and participation.ip is not None \ and not check_ip(self.request.remote_ip, participation.ip): logger.info("Unexpected IP: user=%s pass=%s remote_ip=%s.", filtered_user, filtered_pass, self.request.remote_ip) self.redirect("/?login_error=true") return if participation.hidden and self.contest.block_hidden_participations: logger.info("Hidden user login attempt: " "user=%s pass=%s remote_ip=%s.", filtered_user, filtered_pass, self.request.remote_ip) self.redirect("/?login_error=true") return logger.info("User logged in: user=%s remote_ip=%s.", filtered_user, self.request.remote_ip) self.set_secure_cookie("login", pickle.dumps((user.username, correct_password, make_timestamp())), expires_days=None) self.redirect(next_page)
def safe_validate_password(participation, password): """Check that the password is correct for the authentication. Validate the given password against the participation (using either the global or the contest-specific password that is stored in the database), and guard against a misconfiguration. participation (Participation): a participation. password (str): a password provided by someone trying to log in claiming to be the given participation. return (bool): whether the password matches the expected one. """ if participation.password is None: correct_password = participation.user.password else: correct_password = participation.password try: password_valid = validate_password(correct_password, password) except ValueError as e: # This is either a programming or a configuration error. logger.warning( "Invalid password stored in database for user %s in contest %s: " "%s", participation.user.username, participation.contest.name, e) return False return password_valid
def post(self): username = self.get_argument("username", "") password = self.get_argument("password", "") next_page = self.get_argument("next", "/") admin = self.sql_session.query(Admin)\ .filter(Admin.username == username)\ .first() if admin is None: logger.warning("Nonexistent admin account: %s", username) self.redirect("/login?login_error=true") return try: allowed = validate_password(admin.authentication, password) except ValueError: logger.warning("Unable to validate password for admin %s", filter_ascii(username), exc_info=True) allowed = False if not allowed or not admin.enabled: if not allowed: logger.info("Login error for admin %s from IP %s.", filter_ascii(username), self.request.remote_ip) elif not admin.enabled: logger.info("Login successful for admin %s from IP %s, " "but account is disabled.", filter_ascii(username), self.request.remote_ip) self.redirect("/login?login_error=true") return logger.info("Admin logged in: %s from IP %s.", filter_ascii(username), self.request.remote_ip) self.service.auth_handler.set(admin.id) self.redirect(next_page)
def assertAdminInDb(self, username, pwd, name, enabled, permission_all): """Assert that the admin with the given data is in the DB.""" db_admins = self.session.query(Admin)\ .filter(Admin.username == username).all() self.assertEqual(len(db_admins), 1) a = db_admins[0] self.assertTrue(validate_password(a.authentication, pwd)) self.assertEqual(a.name, name) self.assertEqual(a.enabled, enabled) self.assertEqual(a.permission_all, permission_all)
def perform_login(username, password): with SessionGen() as session: # attempt to get admin for username & check if enabled admin = session.query(Admin).filter_by(username=username).one_or_none() if admin is None or not admin.enabled: abort(401) # attempt to validate password try: allowed = validate_password(admin.authentication, password) except ValueError: allowed = False if not allowed: abort(401) return admin.id
def _get_user(self): username = self.get_argument("username") password = self.get_argument("password") # Find user if it exists user = self.sql_session.query(User)\ .filter(User.username == username)\ .first() if user is None: raise tornado_web.HTTPError(404) # Check if password is correct if not validate_password(user.password, password): raise tornado_web.HTTPError(403) return user
def post(self): error_args = {"login_error": "true"} next_page = self.get_argument("next", None) if next_page is not None: error_args["next"] = next_page if next_page != "/": next_page = self.url(*next_page.strip("/").split("/")) else: next_page = self.url() else: next_page = self.url() error_page = self.url("login", **error_args) username = self.get_argument("username", "") password = self.get_argument("password", "") admin = self.sql_session.query(Admin)\ .filter(Admin.username == username)\ .first() if admin is None: logger.warning("Nonexistent admin account: %s", username) self.redirect(error_page) return try: allowed = validate_password(admin.authentication, password) except ValueError: logger.warning("Unable to validate password for admin %r", username, exc_info=True) allowed = False if not allowed or not admin.enabled: if not allowed: logger.info("Login error for admin %r from IP %s.", username, self.request.remote_ip) elif not admin.enabled: logger.info( "Login successful for admin %r from IP %s, but " "account is disabled.", username, self.request.remote_ip) self.redirect(error_page) return logger.info("Admin logged in: %r from IP %s.", username, self.request.remote_ip) self.service.auth_handler.set(admin.id) self.redirect(next_page)
def post(self): error_args = {"login_error": "true"} next_page = self.get_argument("next", None) if next_page is not None: error_args["next"] = next_page if next_page != "/": next_page = self.url(*next_page.strip("/").split("/")) else: next_page = self.url() else: next_page = self.url() error_page = self.url("login", **error_args) username = self.get_argument("username", "") password = self.get_argument("password", "") admin = self.sql_session.query(Admin)\ .filter(Admin.username == username)\ .first() if admin is None: logger.warning("Nonexistent admin account: %s", username) self.redirect(error_page) return try: allowed = validate_password(admin.authentication, password) except ValueError: logger.warning("Unable to validate password for admin %r", username, exc_info=True) allowed = False if not allowed or not admin.enabled: if not allowed: logger.info("Login error for admin %r from IP %s.", username, self.request.remote_ip) elif not admin.enabled: logger.info("Login successful for admin %r from IP %s, but " "account is disabled.", username, self.request.remote_ip) self.redirect(error_page) return logger.info("Admin logged in: %r from IP %s.", username, self.request.remote_ip) self.service.auth_handler.set(admin.id) self.redirect(next_page)
def assertParticipationInDb(self, user_id, contest_id, password, delay_time=0, extra_time=0, hidden=False, unrestricted=False, ip=None, team_code=None): """Assert that the participation with the given data is in the DB.""" db_participations = self.session.query(Participation)\ .filter(Participation.user_id == user_id)\ .filter(Participation.contest_id == contest_id).all() self.assertEqual(len(db_participations), 1) p = db_participations[0] self.assertTrue(validate_password(p.password, password)) self.assertEqual(p.hidden, hidden) self.assertEqual(p.unrestricted, unrestricted) if ip is None: self.assertIsNone(p.ip) else: assertCountEqual(self, p.ip, ip) if team_code is None: self.assertIsNone(p.team) else: self.assertEqual(p.team.code, team_code)
def test_plaintext(self): self.assertTrue(validate_password( hash_password("p你好", method="plaintext"), "p你好")) self.assertFalse(validate_password( hash_password("p你好", method="plaintext"), "你好"))
def validate_login( sql_session, contest, timestamp, username, password, ip_address): """Authenticate a user logging in, with username and password. Given the information the user provided (the username and the password) and some context information (contest, to determine which users are allowed to log in, how and with which restrictions; timestamp for cookie creation; IP address to check against) try to authenticate the user and return its participation and the cookie to set to help authenticate future visits. After finding the participation, IP login and hidden users restrictions are checked. sql_session (Session): the SQLAlchemy database session used to execute queries. contest (Contest): the contest the user is trying to access. timestamp (datetime): the date and the time of the request. username (str): the username the user provided. password (str): the password the user provided. ip_address (IPv4Address|IPv6Address): the IP address the request came from. return ((Participation, bytes)|(None, None)): if the user couldn't be authenticated then return None, otherwise return the participation that they wanted to authenticate as; if a cookie has to be set return it as well, otherwise return None. """ def log_failed_attempt(msg, *args): logger.info("Unsuccessful login attempt from IP address %s, as user " "%r, on contest %s, at %s: " + msg, ip_address, username, contest.name, timestamp, *args) if not contest.allow_password_authentication: log_failed_attempt("password authentication not allowed") return None, None participation = sql_session.query(Participation) \ .join(Participation.user) \ .options(contains_eager(Participation.user)) \ .filter(Participation.contest == contest)\ .filter(User.username == username)\ .first() if participation is None: log_failed_attempt("user not registered to contest") return None, None correct_password = get_password(participation) try: password_valid = validate_password(correct_password, password) except ValueError as e: # This is either a programming or a configuration error. logger.warning( "Invalid password stored in database for user %s in contest %s: " "%s", participation.user.username, participation.contest.name, e) return None, None if not password_valid: log_failed_attempt("wrong password") return None, None if contest.ip_restriction and participation.ip is not None \ and not any(ip_address in network for network in participation.ip): log_failed_attempt("unauthorized IP address") return None, None if contest.block_hidden_participations and participation.hidden: log_failed_attempt("participation is hidden and unauthorized") return None, None logger.info("Successful login attempt from IP address %s, as user %r, on " "contest %s, at %s", ip_address, username, contest.name, timestamp) # If hashing is used, the cookie stores the hashed password so that # the expensive bcrypt call doesn't need to be done at every request. return (participation, json.dumps([username, correct_password, make_timestamp(timestamp)]) .encode("utf-8"))
def post(self): error_args = {"login_error": "true"} next_page = self.get_argument("next", None) if next_page is not None: error_args["next"] = next_page if next_page != "/": next_page = self.url(*next_page.strip("/").split("/")) else: next_page = self.url() else: next_page = self.contest_url() error_page = self.contest_url(**error_args) username = self.get_argument("username", "") password = self.get_argument("password", "") user = self.sql_session.query(User)\ .filter(User.username == username)\ .first() participation = self.sql_session.query(Participation)\ .filter(Participation.contest == self.contest)\ .filter(Participation.user == user)\ .first() if user is None: # TODO: notify the user that they don't exist self.redirect(error_page) return if participation is None: # TODO: notify the user that they're uninvited self.redirect(error_page) return # If a contest-specific password is defined, use that. If it's # not, use the user's main password. if participation.password is None: correct_password = user.password else: correct_password = participation.password filtered_user = filter_ascii(username) filtered_pass = filter_ascii(password) if not validate_password(correct_password, password): logger.info("Login error: user=%s pass=%s remote_ip=%s." % (filtered_user, filtered_pass, self.request.remote_ip)) self.redirect(error_page) return if self.contest.ip_restriction and participation.ip is not None \ and not check_ip(self.request.remote_ip, participation.ip): logger.info("Unexpected IP: user=%s pass=%s remote_ip=%s.", filtered_user, filtered_pass, self.request.remote_ip) self.redirect(error_page) return if participation.hidden and self.contest.block_hidden_participations: logger.info( "Hidden user login attempt: " "user=%s pass=%s remote_ip=%s.", filtered_user, filtered_pass, self.request.remote_ip) self.redirect(error_page) return logger.info("User logged in: user=%s remote_ip=%s.", filtered_user, self.request.remote_ip) self.set_secure_cookie(self.contest.name + "_login", pickle.dumps((user.username, correct_password, make_timestamp())), expires_days=None) self.redirect(next_page)
def test_invalid_method(self): with self.assertRaises(ValueError): hash_password("test", "pwd") with self.assertRaises(ValueError): validate_password("test:pwd", "pwd")
def test_bcrypt(self): self.assertTrue( validate_password(hash_password("p你好", method="bcrypt"), "p你好")) self.assertFalse( validate_password(hash_password("p你好", method="bcrypt"), "你好"))
def validate_login(sql_session, contest, timestamp, username, password, ip_address): """Authenticate a user logging in, with username and password. Given the information the user provided (the username and the password) and some context information (contest, to determine which users are allowed to log in, how and with which restrictions; timestamp for cookie creation; IP address to check against) try to authenticate the user and return its participation and the cookie to set to help authenticate future visits. After finding the participation, IP login and hidden users restrictions are checked. sql_session (Session): the SQLAlchemy database session used to execute queries. contest (Contest): the contest the user is trying to access. timestamp (datetime): the date and the time of the request. username (str): the username the user provided. password (str): the password the user provided. ip_address (IPv4Address|IPv6Address): the IP address the request came from. return ((Participation, bytes)|(None, None)): if the user couldn't be authenticated then return None, otherwise return the participation that they wanted to authenticate as; if a cookie has to be set return it as well, otherwise return None. """ def log_failed_attempt(msg, *args): logger.info( "Unsuccessful login attempt from IP address %s, as user " "%r, on contest %s, at %s: " + msg, ip_address, username, contest.name, timestamp, *args) if not contest.allow_password_authentication: log_failed_attempt("password authentication not allowed") return None, None participation = sql_session.query(Participation) \ .join(Participation.user) \ .options(contains_eager(Participation.user)) \ .filter(Participation.contest == contest)\ .filter(User.username == username)\ .first() if participation is None: log_failed_attempt("user not registered to contest") return None, None correct_password = get_password(participation) try: password_valid = validate_password(correct_password, password) except ValueError as e: # This is either a programming or a configuration error. logger.warning( "Invalid password stored in database for user %s in contest %s: " "%s", participation.user.username, participation.contest.name, e) return None, None if not password_valid: log_failed_attempt("wrong password") return None, None if contest.ip_restriction and participation.ip is not None \ and not any(ip_address in network for network in participation.ip): log_failed_attempt("unauthorized IP address") return None, None if contest.block_hidden_participations and participation.hidden: log_failed_attempt("participation is hidden and unauthorized") return None, None logger.info( "Successful login attempt from IP address %s, as user %r, on " "contest %s, at %s", ip_address, username, contest.name, timestamp) # If hashing is used, the cookie stores the hashed password so that # the expensive bcrypt call doesn't need to be done at every request. return (participation, json.dumps([username, correct_password, make_timestamp(timestamp)]).encode("utf-8"))
def test_plaintext(self): self.assertTrue( validate_password(hash_password("p你好", method="plaintext"), "p你好")) self.assertFalse( validate_password(hash_password("p你好", method="plaintext"), "你好"))
def test_bcrypt(self): self.assertTrue(validate_password( hash_password("p你好", method="bcrypt"), "p你好")) self.assertFalse(validate_password( hash_password("p你好", method="bcrypt"), "你好"))