def test_unicode_attempts(): user_id = db_utils.create_user(password=u"password", username=user_name) result = login.authenticate_bcrypt(username=user_name, password=u"passwordé", request=None) assert result == (0, 'invalid') result = login.authenticate_bcrypt(username=user_name, password=u"passwörd", request=None) assert result == (0, 'invalid') result = login.authenticate_bcrypt(username=user_name, password=u"password", request=None) assert result == (user_id, None)
def authorize_post_(request): form = request.web_input(credentials='', username='', password='', remember_me='', mobile='', not_me='') try: credentials = json.loads(form.credentials) except ValueError: raise HTTPBadRequest() scopes = credentials.pop('scopes') error = None if form.not_me and form.username: userid, error = login.authenticate_bcrypt( form.username, form.password, request=request if form.remember_me else None) if error: error = errorcode.login_errors.get(error, 'Unknown error.') elif not request.userid: error = "You must specify a username and password." else: userid = request.userid if error: return Response( render_form(request, scopes, credentials, bool(form.mobile), error, form.username, form.password, bool(form.remember_me), bool(form.not_me))) credentials['userid'] = userid response_attrs = server.create_authorization_response( *(extract_params(request) + (scopes, credentials))) return OAuthResponse(*response_attrs)
def POST(self): form = web.input(credentials='', username='', password='', remember_me='', mobile='', not_me='') try: credentials = json.loads(form.credentials) except ValueError: raise web.badrequest() scopes = credentials.pop('scopes') error = None if form.not_me and form.username: userid, error = login.authenticate_bcrypt(form.username, form.password, bool(form.remember_me)) if error: error = errorcode.login_errors.get(error, 'Unknown error.') elif not self.user_id: error = "You must specify a username and password." else: userid = self.user_id if error: return self.render_form(scopes, credentials, bool(form.mobile), error, form.username, form.password, bool(form.remember_me), bool(form.not_me)) credentials['userid'] = userid headers, body, status = server.create_authorization_response( *(extract_params() + (scopes, credentials))) for k, v in headers.iteritems(): web.header(k, v) web.ctx.status = '%s Status' % (status,) return body
def tfa_generate_recovery_codes_verify_password_post_(request): userid, status = login.authenticate_bcrypt(define.get_display_name(request.userid), request.params['password'], request=None) # The user's password failed to authenticate if status == "invalid": return Response(define.webpage( request.userid, "control/2fa/generate_recovery_codes_verify_password.html", ["password"], title="Generate Recovery Codes: Verify Password" )) # The user has authenticated, so continue with generating the new recovery codes. else: # Edge case prevention: Stop the user from having two Weasyl sessions open and trying # to proceed through the generation process with two sets of recovery codes. invalidate_other_sessions(request.userid) # Edge case prevention: Do we have existing (and recent) codes on this session? Prevent # a user from confusing themselves if they visit the request page twice. sess = request.weasyl_session gen_rec_codes = True if '2fa_recovery_codes_timestamp' in sess.additional_data: # Are the codes on the current session < 30 minutes old? tstamp = sess.additional_data['2fa_recovery_codes_timestamp'] if arrow.now().timestamp - tstamp < 1800: # We have recent codes on the session, use them instead of generating fresh codes. recovery_codes = sess.additional_data['2fa_recovery_codes'].split(',') gen_rec_codes = False if gen_rec_codes: # Either this is a fresh request to generate codes, or the timelimit was exceeded. recovery_codes = tfa.generate_recovery_codes() _set_recovery_codes_on_session(','.join(recovery_codes)) return Response(define.webpage(request.userid, "control/2fa/generate_recovery_codes.html", [ recovery_codes, None ], title="Generate Recovery Codes: Save New Recovery Codes"))
def tfa_init_post_(request): userid, status = login.authenticate_bcrypt(define.get_display_name( request.userid), request.params['password'], request=None) # The user's password failed to authenticate if status == "invalid": return Response( define.webpage( request.userid, "control/2fa/init.html", [define.get_display_name(request.userid), "password"], title="Enable 2FA: Step 1")) # Unlikely that this block will get triggered, but just to be safe, check for it elif status == "unicode-failure": raise HTTPSeeOther(location='/signin/unicode-failure') # The user has authenticated, so continue with the initialization process. else: tfa_secret, tfa_qrcode = tfa.init(request.userid) _set_totp_code_on_session(tfa_secret) return Response( define.webpage(request.userid, "control/2fa/init_qrcode.html", [ define.get_display_name(request.userid), tfa_secret, tfa_qrcode, None ], title="Enable 2FA: Step 2"))
def test_login_fails_if_user_is_banned(): user_id = db_utils.create_user(password=raw_password, username=user_name) d.engine.execute("UPDATE login SET settings = 'b' WHERE userid = %(id)s", id=user_id) result = login.authenticate_bcrypt(username=user_name, password=raw_password) assert result == (user_id, 'banned')
def test_login_fails_for_invalid_auth_and_logs_failure_if_mod_account( tmpdir, monkeypatch): # Required: Monkeypatch the log directory, and the staff.MODS frozenset monkeypatch.setenv(macro.MACRO_SYS_LOG_PATH, tmpdir + "/") log_path = '%s%s.%s.log' % (macro.MACRO_SYS_LOG_PATH, 'login.fail', d.get_timestamp()) user_id = db_utils.create_user(username='******', password=raw_password) # Set the moderators in libweasyl/staff.py via monkeypatch monkeypatch.setattr(staff, 'MODS', frozenset([user_id])) # Ensure we are actually writing to the file by counting the file's lines prerun_loglines = 0 # The file might not exist; this is fine; ignore try: with open(log_path, 'r') as log: for line in log: prerun_loglines += 1 log.close() except IOError: pass postrun_loglines = 0 # Item under test result = login.authenticate_bcrypt(username='******', password='******', request=None) # Verify we are writing to the log file as expected with open(log_path, 'r') as log: for line in log: postrun_loglines += 1 last_line = line log.close() last_line_dict = json.loads(last_line) assert postrun_loglines > prerun_loglines assert last_line_dict['userid'] == user_id assert result == (0, 'invalid')
def signin_post_(request): form = request.web_input(username="", password="", referer="", sfwmode="nsfw") form.referer = form.referer or '/' logid, logerror = login.authenticate_bcrypt(form.username, form.password) if logid and logerror == 'unicode-failure': raise HTTPSeeOther(location='/signin/unicode-failure') elif logid and logerror is None: if form.sfwmode == "sfw": request.set_cookie_on_response("sfwmode", "sfw", 31536000) # Invalidate cached versions of the frontpage to respect the possibly changed SFW settings. index.template_fields.invalidate(logid) raise HTTPSeeOther(location=form.referer) elif logerror == "invalid": return Response(define.webpage(request.userid, "etc/signin.html", [True, form.referer])) elif logerror == "banned": reason = moderation.get_ban_reason(logid) return Response(define.errorpage( request.userid, "Your account has been permanently banned and you are no longer allowed " "to sign in.\n\n%s\n\nIf you believe this ban is in error, please " "contact [email protected] for assistance." % (reason,))) elif logerror == "suspended": suspension = moderation.get_suspension(logid) return Response(define.errorpage( request.userid, "Your account has been temporarily suspended and you are not allowed to " "be logged in at this time.\n\n%s\n\nThis suspension will be lifted on " "%s.\n\nIf you believe this suspension is in error, please contact " "[email protected] for assistance." % (suspension.reason, define.convert_date(suspension.release)))) elif logerror == "address": return Response("IP ADDRESS TEMPORARILY BLOCKED") return Response(define.errorpage(request.userid))
def edit_email_password(userid, username, password, newemail, newemailcheck, newpassword, newpasscheck): from weasyl import login # Check that credentials are correct logid, logerror = login.authenticate_bcrypt(username, password, session=False) if userid != logid or logerror is not None: raise WeasylError("loginInvalid") if newemail: if newemail != newemailcheck: raise WeasylError("emailMismatch") elif login.email_exists(newemail): raise WeasylError("emailExists") if newpassword: if newpassword != newpasscheck: raise WeasylError("passwordMismatch") elif not login.password_secure(newpassword): raise WeasylError("passwordInsecure") if newemail: d.execute("UPDATE login SET email = '%s' WHERE userid = %i", [newemail, userid]) if newpassword: d.execute("UPDATE authbcrypt SET hashsum = '%s' WHERE userid = %i", [login.passhash(newpassword), userid])
def test_login_fails_for_invalid_auth_and_logs_failure_if_mod_account(tmpdir, monkeypatch): # Required: Monkeypatch the log directory, and the staff.MODS frozenset monkeypatch.setenv(macro.MACRO_SYS_LOG_PATH, tmpdir + "/") log_path = '%s%s.%s.log' % (macro.MACRO_SYS_LOG_PATH, 'login.fail', d.get_timestamp()) user_id = db_utils.create_user(username='******', password=raw_password) # Set the moderators in libweasyl/staff.py via monkeypatch monkeypatch.setattr(staff, 'MODS', frozenset([user_id])) # Ensure we are actually writing to the file by counting the file's lines prerun_loglines = 0 # The file might not exist; this is fine; ignore try: with open(log_path, 'r') as log: for line in log: prerun_loglines += 1 log.close() except IOError: pass postrun_loglines = 0 # Item under test result = login.authenticate_bcrypt(username='******', password='******', request=None) # Verify we are writing to the log file as expected with open(log_path, 'r') as log: for line in log: postrun_loglines += 1 last_line = line log.close() last_line_dict = json.loads(last_line) assert postrun_loglines > prerun_loglines assert last_line_dict['userid'] == user_id assert result == (0, 'invalid')
def test_login_fails_for_incorrect_credentials(): random_password = "******" db_utils.create_user(password=random_password, username=user_name) another_random_password = "******" result = login.authenticate_bcrypt(username=user_name, password=another_random_password) assert result == (0, 'invalid')
def POST(self): form = web.input(username="", password="", referer="", sfwmode="nsfw") form.referer = form.referer or '/index' logid, logerror = login.authenticate_bcrypt(form.username, form.password) if logid and logerror == 'unicode-failure': raise web.seeother('/signin/unicode-failure') elif logid and logerror is None: if form.sfwmode == "sfw": web.setcookie("sfwmode", "sfw", 31536000) raise web.seeother(form.referer) elif logerror == "invalid": return define.webpage(self.user_id, template.etc_signin, [True, form.referer]) elif logerror == "banned": reason = moderation.get_ban_reason(logid) return define.errorpage( self.user_id, "Your account has been permanently banned and you are no longer allowed " "to sign in.\n\n%s\n\nIf you believe this ban is in error, please " "contact [email protected] for assistance." % (reason,)) elif logerror == "suspended": suspension = moderation.get_suspension(logid) return define.errorpage( self.user_id, "Your account has been temporarily suspended and you are not allowed to " "be logged in at this time.\n\n%s\n\nThis suspension will be lifted on " "%s.\n\nIf you believe this suspension is in error, please contact " "[email protected] for assistance." % (suspension.reason, define.convert_date(suspension.release))) elif logerror == "address": return "IP ADDRESS TEMPORARILY BLOCKED" return define.errorpage(self.user_id)
def test_login_fails_if_user_is_banned(): user_id = db_utils.create_user(password=raw_password, username=user_name) db_utils.create_banuser(userid=user_id, reason="Testing") result = login.authenticate_bcrypt(username=user_name, password=raw_password, request=None) assert result == (user_id, 'banned')
def test_login_fails_if_user_is_suspended(): user_id = db_utils.create_user(password=raw_password, username=user_name) d.engine.execute("UPDATE login SET settings = 's' WHERE userid = %(id)s", id=user_id) release_date = d.get_time() + 60 d.engine.execute("INSERT INTO suspension VALUES (%(id)s, %(reason)s, %(rel)s)", id=user_id, reason='test', rel=release_date) result = login.authenticate_bcrypt(username=user_name, password=raw_password, request=None) assert result == (user_id, 'suspended')
def test_verify_success_if_correct_information_provided(): user_name = 'test' user_id = db_utils.create_user(password='******', username=user_name) password = '******' form = Bag(password=password, passcheck=password) resetpassword.force(user_id, form) result = login.authenticate_bcrypt(username=user_name, password=password, session=False) assert result == (user_id, None)
def test_login_succeeds_if_suspension_duration_has_expired(): user_id = db_utils.create_user(password=raw_password, username=user_name) d.engine.execute("UPDATE login SET settings = 's' WHERE userid = %(id)s", id=user_id) release_date = d.convert_unixdate(31, 12, 2015) d.engine.execute("INSERT INTO suspension VALUES (%(id)s, %(reason)s, %(rel)s)", id=user_id, reason='test', rel=release_date) result = login.authenticate_bcrypt(username=user_name, password=raw_password, request=None) assert result == (user_id, None)
def test_logins_with_unicode_failures_succeed_with_corresponding_status(): user_id = db_utils.create_user(password=raw_password, username=user_name) # This hash corresponds to "password" old_2a_bcrypt_hash = '$2a$04$uOBx2JziJoxjq/F88NjQZ.8mRGE8FgLi3q0Rm3QUlBZnhhInXCb9K' d.engine.execute("UPDATE authbcrypt SET hashsum = %(hash)s WHERE userid = %(id)s", hash=old_2a_bcrypt_hash, id=user_id) result = login.authenticate_bcrypt(user_name, u"passwordé", request=None) assert result == (user_id, 'unicode-failure')
def test_verify_success_if_correct_information_provided(): user_name = 'test' user_id = db_utils.create_user(password='******', username=user_name) password = '******' form = Bag(password=password, passcheck=password) resetpassword.force(user_id, form) result = login.authenticate_bcrypt(username=user_name, password=password, request=None) assert result == (user_id, None)
def signin_post_(request): form = request.web_input(username="", password="", referer="", sfwmode="nsfw") form.referer = form.referer or '/' logid, logerror = login.authenticate_bcrypt(form.username, form.password, request=request, ip_address=request.client_addr, user_agent=request.user_agent) if logid and logerror is None: if form.sfwmode == "sfw": request.set_cookie_on_response("sfwmode", "sfw", 31536000) # Invalidate cached versions of the frontpage to respect the possibly changed SFW settings. index.template_fields.invalidate(logid) raise HTTPSeeOther(location=form.referer) elif logid and logerror == "2fa": # Password authentication passed, but user has 2FA set, so verify second factor (Also set SFW mode now) if form.sfwmode == "sfw": request.set_cookie_on_response("sfwmode", "sfw", 31536000) index.template_fields.invalidate(logid) # Check if out of recovery codes; this should *never* execute normally, save for crafted # webtests. However, check for it and log an error to Sentry if it happens. remaining_recovery_codes = two_factor_auth.get_number_of_recovery_codes(logid) if remaining_recovery_codes == 0: raise RuntimeError("Two-factor Authentication: Count of recovery codes for userid " + str(logid) + " was zero upon password authentication succeeding, " + "which should be impossible.") # Store the authenticated userid & password auth time to the session sess = define.get_weasyl_session() # The timestamp at which password authentication succeeded sess.additional_data['2fa_pwd_auth_timestamp'] = arrow.now().timestamp # The userid of the user attempting authentication sess.additional_data['2fa_pwd_auth_userid'] = logid # The number of times the user has attempted to authenticate via 2FA sess.additional_data['2fa_pwd_auth_attempts'] = 0 sess.save = True return Response(define.webpage( request.userid, "etc/signin_2fa_auth.html", [define.get_display_name(logid), form.referer, remaining_recovery_codes, None], title="Sign In - 2FA" )) elif logerror == "invalid": return Response(define.webpage(request.userid, "etc/signin.html", [True, form.referer])) elif logerror == "banned": reason = moderation.get_ban_reason(logid) return Response(define.errorpage( request.userid, "Your account has been permanently banned and you are no longer allowed " "to sign in.\n\n%s\n\nIf you believe this ban is in error, please " "contact %s for assistance." % (reason, MACRO_SUPPORT_ADDRESS))) elif logerror == "suspended": suspension = moderation.get_suspension(logid) return Response(define.errorpage( request.userid, "Your account has been temporarily suspended and you are not allowed to " "be logged in at this time.\n\n%s\n\nThis suspension will be lifted on " "%s.\n\nIf you believe this suspension is in error, please contact " "%s for assistance." % (suspension.reason, define.convert_date(suspension.release), MACRO_SUPPORT_ADDRESS))) raise WeasylError("Unexpected") # pragma: no cover
def test_login_succeeds_if_suspension_duration_has_expired(): user_id = db_utils.create_user(password=raw_password, username=user_name) release_date = d.convert_unixdate(31, 12, 2015) db_utils.create_suspenduser(userid=user_id, reason="Testing", release=release_date) result = login.authenticate_bcrypt(username=user_name, password=raw_password, request=None) assert result == (user_id, None)
def test_login_fails_if_user_is_suspended(): user_id = db_utils.create_user(password=raw_password, username=user_name) release_date = d.get_time() + 60 db_utils.create_suspenduser(userid=user_id, reason="Testing", release=release_date) result = login.authenticate_bcrypt(username=user_name, password=raw_password, request=None) assert result == (user_id, 'suspended')
def test_login_fails_if_user_is_suspended(): user_id = db_utils.create_user(password=raw_password, username=user_name) d.engine.execute("UPDATE login SET settings = 's' WHERE userid = %(id)s", id=user_id) release_date = d.get_time() + 60 d.engine.execute( "INSERT INTO suspension VALUES (%(id)s, %(reason)s, %(rel)s)", id=user_id, reason='test', rel=release_date) result = login.authenticate_bcrypt(username=user_name, password=raw_password, request=None) assert result == (user_id, 'suspended')
def test_login_succeeds_if_suspension_duration_has_expired(): user_id = db_utils.create_user(password=raw_password, username=user_name) d.engine.execute("UPDATE login SET settings = 's' WHERE userid = %(id)s", id=user_id) release_date = d.convert_unixdate(31, 12, 2015) d.engine.execute( "INSERT INTO suspension VALUES (%(id)s, %(reason)s, %(rel)s)", id=user_id, reason='test', rel=release_date) result = login.authenticate_bcrypt(username=user_name, password=raw_password, request=None) assert result == (user_id, None)
def tfa_init_post_(request): userid, status = login.authenticate_bcrypt(define.get_display_name(request.userid), request.params['password'], request=None) # The user's password failed to authenticate if status == "invalid": return Response(define.webpage(request.userid, "control/2fa/init.html", [ define.get_display_name(request.userid), "password" ], title="Enable 2FA: Step 1")) # The user has authenticated, so continue with the initialization process. else: tfa_secret, tfa_qrcode = tfa.init(request.userid) _set_totp_code_on_session(tfa_secret) return Response(define.webpage(request.userid, "control/2fa/init_qrcode.html", [ define.get_display_name(request.userid), tfa_secret, tfa_qrcode, None ], title="Enable 2FA: Step 2"))
def signin_post_(request): form = request.web_input(username="", password="", referer="", sfwmode="nsfw") form.referer = form.referer or '/' logid, logerror = login.authenticate_bcrypt(form.username, form.password) if logid and logerror == 'unicode-failure': raise HTTPSeeOther(location='/signin/unicode-failure') elif logid and logerror is None: if form.sfwmode == "sfw": request.set_cookie_on_response("sfwmode", "sfw", 31536000) # Invalidate cached versions of the frontpage to respect the possibly changed SFW settings. index.template_fields.invalidate(logid) raise HTTPSeeOther(location=form.referer) elif logerror == "invalid": return Response( define.webpage(request.userid, "etc/signin.html", [True, form.referer])) elif logerror == "banned": reason = moderation.get_ban_reason(logid) return Response( define.errorpage( request.userid, "Your account has been permanently banned and you are no longer allowed " "to sign in.\n\n%s\n\nIf you believe this ban is in error, please " "contact [email protected] for assistance." % (reason, ))) elif logerror == "suspended": suspension = moderation.get_suspension(logid) return Response( define.errorpage( request.userid, "Your account has been temporarily suspended and you are not allowed to " "be logged in at this time.\n\n%s\n\nThis suspension will be lifted on " "%s.\n\nIf you believe this suspension is in error, please contact " "[email protected] for assistance." % (suspension.reason, define.convert_date(suspension.release)))) elif logerror == "address": return Response("IP ADDRESS TEMPORARILY BLOCKED") return Response(define.errorpage(request.userid))
def tfa_init_post_(request): userid, status = login.authenticate_bcrypt(define.get_display_name(request.userid), request.params['password'], request=None) # The user's password failed to authenticate if status == "invalid": return Response(define.webpage(request.userid, "control/2fa/init.html", [ define.get_display_name(request.userid), "password" ], title="Enable 2FA: Step 1")) # Unlikely that this block will get triggered, but just to be safe, check for it elif status == "unicode-failure": raise HTTPSeeOther(location='/signin/unicode-failure') # The user has authenticated, so continue with the initialization process. else: tfa_secret, tfa_qrcode = tfa.init(request.userid) _set_totp_code_on_session(tfa_secret) return Response(define.webpage(request.userid, "control/2fa/init_qrcode.html", [ define.get_display_name(request.userid), tfa_secret, tfa_qrcode, None ], title="Enable 2FA: Step 2"))
def POST(self): form = web.input(username="", password="", referer="", sfwmode="nsfw") form.referer = form.referer or '/index' logid, logerror = login.authenticate_bcrypt(form.username, form.password) if logid and logerror == 'unicode-failure': raise web.seeother('/signin/unicode-failure') elif logid and logerror is None: if form.sfwmode == "sfw": web.setcookie("sfwmode", "sfw", 31536000) raise web.seeother(form.referer) elif logerror == "invalid": return define.webpage(self.user_id, template.etc_signin, [True, form.referer]) elif logerror == "banned": reason = moderation.get_ban_reason(logid) return define.errorpage( self.user_id, "Your account has been permanently banned and you are no longer allowed " "to sign in.\n\n%s\n\nIf you believe this ban is in error, please " "contact [email protected] for assistance." % (reason, )) elif logerror == "suspended": suspension = moderation.get_suspension(logid) return define.errorpage( self.user_id, "Your account has been temporarily suspended and you are not allowed to " "be logged in at this time.\n\n%s\n\nThis suspension will be lifted on " "%s.\n\nIf you believe this suspension is in error, please contact " "[email protected] for assistance." % (suspension.reason, define.convert_date(suspension.release))) elif logerror == "address": return "IP ADDRESS TEMPORARILY BLOCKED" return define.errorpage(self.user_id)
def authorize_post_(request): form = request.web_input(credentials='', username='', password='', remember_me='', mobile='', not_me='') try: credentials = json.loads(form.credentials) except ValueError: raise HTTPBadRequest() scopes = credentials.pop('scopes') error = None if form.not_me and form.username: userid, error = login.authenticate_bcrypt(form.username, form.password, bool(form.remember_me)) if error: error = errorcode.login_errors.get(error, 'Unknown error.') elif not request.userid: error = "You must specify a username and password." else: userid = request.userid if error: return Response(render_form(request, scopes, credentials, bool(form.mobile), error, form.username, form.password, bool(form.remember_me), bool(form.not_me))) credentials['userid'] = userid response_attrs = server.create_authorization_response( *(extract_params(request) + (scopes, credentials))) return OAuthResponse(*response_attrs)
def test_login_fails_if_no_password_provided(): result = login.authenticate_bcrypt(username=user_name, password='', request=None) assert result == (0, 'invalid')
def test_login_succeeds_for_valid_username_and_password(): user_id = db_utils.create_user(password=raw_password, username=user_name) result = login.authenticate_bcrypt(username=user_name, password=raw_password, request=None) assert result == (user_id, None)
def test_login_fails_if_user_is_banned(): user_id = db_utils.create_user(password=raw_password, username=user_name) d.engine.execute("UPDATE login SET settings = 'b' WHERE userid = %(id)s", id=user_id) result = login.authenticate_bcrypt(username=user_name, password=raw_password, request=None) assert result == (user_id, 'banned')
def signin_post_(request): form = request.web_input(username="", password="", referer="", sfwmode="nsfw") form.referer = form.referer or '/' logid, logerror = login.authenticate_bcrypt(form.username, form.password, request=request, ip_address=request.client_addr, user_agent=request.user_agent) if logid and logerror == 'unicode-failure': raise HTTPSeeOther(location='/signin/unicode-failure') elif logid and logerror is None: if form.sfwmode == "sfw": request.set_cookie_on_response("sfwmode", "sfw", 31536000) # Invalidate cached versions of the frontpage to respect the possibly changed SFW settings. index.template_fields.invalidate(logid) raise HTTPSeeOther(location=form.referer) elif logid and logerror == "2fa": # Password authentication passed, but user has 2FA set, so verify second factor (Also set SFW mode now) if form.sfwmode == "sfw": request.set_cookie_on_response("sfwmode", "sfw", 31536000) index.template_fields.invalidate(logid) # Check if out of recovery codes; this should *never* execute normally, save for crafted # webtests. However, check for it and log an error to Sentry if it happens. remaining_recovery_codes = two_factor_auth.get_number_of_recovery_codes(logid) if remaining_recovery_codes == 0: raise RuntimeError("Two-factor Authentication: Count of recovery codes for userid " + str(logid) + " was zero upon password authentication succeeding, " + "which should be impossible.") # Store the authenticated userid & password auth time to the session sess = define.get_weasyl_session() # The timestamp at which password authentication succeeded sess.additional_data['2fa_pwd_auth_timestamp'] = arrow.now().timestamp # The userid of the user attempting authentication sess.additional_data['2fa_pwd_auth_userid'] = logid # The number of times the user has attempted to authenticate via 2FA sess.additional_data['2fa_pwd_auth_attempts'] = 0 sess.save = True return Response(define.webpage( request.userid, "etc/signin_2fa_auth.html", [define.get_display_name(logid), form.referer, remaining_recovery_codes, None], title="Sign In - 2FA" )) elif logerror == "invalid": return Response(define.webpage(request.userid, "etc/signin.html", [True, form.referer])) elif logerror == "banned": reason = moderation.get_ban_reason(logid) return Response(define.errorpage( request.userid, "Your account has been permanently banned and you are no longer allowed " "to sign in.\n\n%s\n\nIf you believe this ban is in error, please " "contact [email protected] for assistance." % (reason,))) elif logerror == "suspended": suspension = moderation.get_suspension(logid) return Response(define.errorpage( request.userid, "Your account has been temporarily suspended and you are not allowed to " "be logged in at this time.\n\n%s\n\nThis suspension will be lifted on " "%s.\n\nIf you believe this suspension is in error, please contact " "[email protected] for assistance." % (suspension.reason, define.convert_date(suspension.release)))) elif logerror == "address": return Response("IP ADDRESS TEMPORARILY BLOCKED") return Response(define.errorpage(request.userid))
def test_login_fails_for_incorrect_credentials(): random_password = "******" db_utils.create_user(password=random_password, username=user_name) another_random_password = "******" result = login.authenticate_bcrypt(username=user_name, password=another_random_password, request=None) assert result == (0, 'invalid')
def edit_email_password(userid, username, password, newemail, newemailcheck, newpassword, newpasscheck): """ Edit the email address and/or password for a given Weasyl account. After verifying the user's current login credentials, edit the user's email address and/or password if validity checks pass. If the email is modified, a confirmation email is sent to the user's target email with a token which, if used, finalizes the email address change. Parameters: userid: The `userid` of the Weasyl account to modify. username: User-entered username for password-based authentication. password: The user's current plaintext password. newemail: If changing the email on the account, the new email address. Optional. newemailcheck: A verification field for the above to serve as a typo-check. Optional, but mandatory if `newemail` provided. newpassword: If changing the password, the user's new password. Optional. newpasswordcheck: Verification field for `newpassword`. Optional, but mandatory if `newpassword` provided. """ from weasyl import login # Track if any changes were made for later display back to the user. changes_made = "" # Check that credentials are correct logid, logerror = login.authenticate_bcrypt(username, password, request=None) # Run checks prior to modifying anything... if userid != logid or logerror is not None: raise WeasylError("loginInvalid") if newemail: if newemail != newemailcheck: raise WeasylError("emailMismatch") if newpassword: if newpassword != newpasscheck: raise WeasylError("passwordMismatch") elif not login.password_secure(newpassword): raise WeasylError("passwordInsecure") # If we are setting a new email, then write the email into a holding table pending confirmation # that the email is valid. if newemail: # Only actually attempt to change the email if unused; prevent finding out if an email is already registered if not login.email_exists(newemail): token = security.generate_key(40) # Store the current token & email, updating them to overwrite a previous attempt if needed d.engine.execute(""" INSERT INTO emailverify (userid, email, token, createtimestamp) VALUES (%(userid)s, %(newemail)s, %(token)s, NOW()) ON CONFLICT (userid) DO UPDATE SET email = %(newemail)s, token = %(token)s, createtimestamp = NOW() """, userid=userid, newemail=newemail, token=token) # Send out the email containing the verification token. emailer.append([newemail], None, "Weasyl Email Change Confirmation", d.render("email/verify_emailchange.html", [token, d.get_display_name(userid)])) else: # The target email exists: let the target know this query_username = d.engine.scalar(""" SELECT login_name FROM login WHERE email = %(email)s """, email=newemail) emailer.append([newemail], None, "Weasyl Account Information - Duplicate Email on Accounts Rejected", d.render( "email/email_in_use_email_change.html", [query_username]) ) # Then add text to `changes_made` telling that we have completed the email change request, and how to proceed. changes_made += "Your email change request is currently pending. An email has been sent to **" + newemail + "**. Follow the instructions within to finalize your email address change.\n" # If the password is being updated, update the hash, and clear other sessions. if newpassword: d.execute("UPDATE authbcrypt SET hashsum = '%s' WHERE userid = %i", [login.passhash(newpassword), userid]) # Invalidate all sessions for `userid` except for the current one invalidate_other_sessions(userid) # Then add to `changes_made` detailing that the password change has successfully occurred. changes_made += "Your password has been successfully changed. As a security precaution, you have been logged out of all other active sessions." if changes_made != "": return changes_made else: return False
def test_login_fails_if_incorrect_username_is_provided(): result = login.authenticate_bcrypt(username=user_name, password=raw_password, request=None) assert result == (0, 'invalid')
def test_login_fails_if_no_username_provided(): result = login.authenticate_bcrypt(username='', password='******') assert result == (0, 'invalid')
def edit_email_password(userid, username, password, newemail, newemailcheck, newpassword, newpasscheck): """ Edit the email address and/or password for a given Weasyl account. After verifying the user's current login credentials, edit the user's email address and/or password if validity checks pass. If the email is modified, a confirmation email is sent to the user's target email with a token which, if used, finalizes the email address change. Parameters: userid: The `userid` of the Weasyl account to modify. username: User-entered username for password-based authentication. password: The user's current plaintext password. newemail: If changing the email on the account, the new email address. Optional. newemailcheck: A verification field for the above to serve as a typo-check. Optional, but mandatory if `newemail` provided. newpassword: If changing the password, the user's new password. Optional. newpasswordcheck: Verification field for `newpassword`. Optional, but mandatory if `newpassword` provided. """ from weasyl import login # Track if any changes were made for later display back to the user. changes_made = "" # Check that credentials are correct logid, logerror = login.authenticate_bcrypt(username, password, request=None) # Run checks prior to modifying anything... if userid != logid or logerror is not None: raise WeasylError("loginInvalid") if newemail: if newemail != newemailcheck: raise WeasylError("emailMismatch") if newpassword: if newpassword != newpasscheck: raise WeasylError("passwordMismatch") elif not login.password_secure(newpassword): raise WeasylError("passwordInsecure") # If we are setting a new email, then write the email into a holding table pending confirmation # that the email is valid. if newemail: # Only actually attempt to change the email if unused; prevent finding out if an email is already registered if not login.email_exists(newemail): token = security.generate_key(40) # Store the current token & email, updating them to overwrite a previous attempt if needed d.engine.execute(""" INSERT INTO emailverify (userid, email, token, createtimestamp) VALUES (%(userid)s, %(newemail)s, %(token)s, NOW()) ON CONFLICT (userid) DO UPDATE SET email = %(newemail)s, token = %(token)s, createtimestamp = NOW() """, userid=userid, newemail=newemail, token=token) # Send out the email containing the verification token. emailer.append([newemail], None, "Weasyl Email Change Confirmation", d.render("email/verify_emailchange.html", [token, d.get_display_name(userid)])) else: # The target email exists: let the target know this query_username = d.engine.scalar(""" SELECT login_name FROM login WHERE email = %(email)s """, email=newemail) emailer.append( [newemail], None, "Weasyl Account Information - Duplicate Email on Accounts Rejected", d.render("email/email_in_use_email_change.html", [query_username])) # Then add text to `changes_made` telling that we have completed the email change request, and how to proceed. changes_made += "Your email change request is currently pending. An email has been sent to **" + newemail + "**. Follow the instructions within to finalize your email address change.\n" # If the password is being updated, update the hash, and clear other sessions. if newpassword: d.execute("UPDATE authbcrypt SET hashsum = '%s' WHERE userid = %i", [login.passhash(newpassword), userid]) # Invalidate all sessions for `userid` except for the current one invalidate_other_sessions(userid) # Then add to `changes_made` detailing that the password change has successfully occurred. changes_made += "Your password has been successfully changed. As a security precaution, you have been logged out of all other active sessions." if changes_made != "": return changes_made else: return False