def tfa_generate_recovery_codes_verify_password_post_(request): userid, status = login.authenticate_bcrypt(define.get_display_name(request.userid), request.params['password'], session=False) # 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 = define.get_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 signin_2fa_auth_get_(request): sess = define.get_weasyl_session() # Only render page if the password has been authenticated (we have a UserID stored in the session) if '2fa_pwd_auth_userid' not in sess.additional_data: return Response(define.errorpage(request.userid, errorcode.permission)) tfa_userid = sess.additional_data['2fa_pwd_auth_userid'] # Maximum secondary authentication time: 5 minutes session_life = arrow.now( ).timestamp - sess.additional_data['2fa_pwd_auth_timestamp'] if session_life > 300: _cleanup_2fa_session() return Response( define.errorpage( request.userid, errorcode. error_messages['TwoFactorAuthenticationAuthenticationTimeout'], [["Sign In", "/signin"], ["Return to the Home Page", "/"]])) else: ref = request.params["referer"] if "referer" in request.params else "/" return Response( define.webpage( request.userid, "etc/signin_2fa_auth.html", [ define.get_display_name(tfa_userid), ref, two_factor_auth.get_number_of_recovery_codes(tfa_userid), None ], title="Sign In - 2FA"))
def signin_2fa_auth_get_(request): sess = define.get_weasyl_session() # Only render page if the session exists //and// the password has # been authenticated (we have a UserID stored in the session) if not sess.additional_data or '2fa_pwd_auth_userid' not in sess.additional_data: raise WeasylError('InsufficientPermissions') tfa_userid = sess.additional_data['2fa_pwd_auth_userid'] # Maximum secondary authentication time: 5 minutes session_life = arrow.now( ).timestamp - sess.additional_data['2fa_pwd_auth_timestamp'] if session_life > 300: _cleanup_2fa_session() raise WeasylError('TwoFactorAuthenticationAuthenticationTimeout') else: ref = request.params["referer"] if "referer" in request.params else "/" return Response( define.webpage( request.userid, "etc/signin_2fa_auth.html", [ define.get_display_name(tfa_userid), ref, two_factor_auth.get_number_of_recovery_codes(tfa_userid), None ], title="Sign In - 2FA"))
def signin(userid): # Update the last login record for the user d.execute("UPDATE login SET last_login = %i WHERE userid = %i", [d.get_time(), userid]) # set the userid on the session sess = d.get_weasyl_session() sess.userid = userid sess.save = True
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 _cleanup_session(): sess = define.get_weasyl_session() if '2fa_recovery_codes' in sess.additional_data: del sess.additional_data['2fa_recovery_codes'] if '2fa_recovery_codes_timestamp' in sess.additional_data: del sess.additional_data['2fa_recovery_codes_timestamp'] if '2fa_totp_code' in sess.additional_data: del sess.additional_data['2fa_totp_code'] sess.save = True
def signin_2fa_auth_post_(request): sess = define.get_weasyl_session() # Only render page if the password has been authenticated (we have a UserID stored in the session) if '2fa_pwd_auth_userid' not in sess.additional_data: return Response(define.errorpage(request.userid, errorcode.permission)) tfa_userid = sess.additional_data['2fa_pwd_auth_userid'] session_life = arrow.now( ).timestamp - sess.additional_data['2fa_pwd_auth_timestamp'] if session_life > 300: # Maximum secondary authentication time: 5 minutes _cleanup_2fa_session() return Response( define.errorpage( request.userid, errorcode. error_messages['TwoFactorAuthenticationAuthenticationTimeout'], [["Sign In", "/signin"], ["Return to the Home Page", "/"]])) elif two_factor_auth.verify(tfa_userid, request.params["tfaresponse"]): # 2FA passed, so login and cleanup. _cleanup_2fa_session() login.signin(tfa_userid) ref = request.params["referer"] or "/" # User is out of recovery codes, so force-deactivate 2FA if two_factor_auth.get_number_of_recovery_codes(tfa_userid) == 0: two_factor_auth.force_deactivate(tfa_userid) return Response( define.errorpage( tfa_userid, errorcode.error_messages[ 'TwoFactorAuthenticationZeroRecoveryCodesRemaining'], [["2FA Dashboard", "/control/2fa/status"], ["Return to the Home Page", "/"]])) # Return to the target page, restricting to the path portion of 'ref' per urlparse. raise HTTPSeeOther(location=urlparse.urlparse(ref).path) elif sess.additional_data['2fa_pwd_auth_attempts'] >= 5: # Hinder brute-forcing the 2FA token or recovery code by enforcing an upper-bound on 2FA auth attempts. _cleanup_2fa_session() return Response( define.errorpage( request.userid, errorcode.error_messages[ 'TwoFactorAuthenticationAuthenticationAttemptsExceeded'], [["Sign In", "/signin"], ["Return to the Home Page", "/"]])) else: # Log the failed authentication attempt to the session and save sess.additional_data['2fa_pwd_auth_attempts'] += 1 sess.save = True # 2FA failed; redirect to 2FA input page & inform user that authentication failed. return Response( define.webpage( request.userid, "etc/signin_2fa_auth.html", [ define.get_display_name(tfa_userid), request.params["referer"], two_factor_auth.get_number_of_recovery_codes(tfa_userid), "2fa" ], title="Sign In - 2FA"))
def signin(userid): # Update the last login record for the user d.execute("UPDATE login SET last_login = %i WHERE userid = %i", [d.get_time(), userid]) # Log the successful login and increment the login count d.append_to_log('login.success', userid=userid, ip=d.get_address()) d.metric('increment', 'logins') # set the userid on the session sess = d.get_weasyl_session() sess.userid = userid sess.save = True
def _cleanup_2fa_session(): """ Cleans up a Weasyl session of any 2FA data stored during the authentication process. Parameters: None; keys off of the currently active session making the request. Returns: Nothing. """ sess = define.get_weasyl_session() del sess.additional_data['2fa_pwd_auth_timestamp'] del sess.additional_data['2fa_pwd_auth_userid'] del sess.additional_data['2fa_pwd_auth_attempts'] sess.save = True
def signin_2fa_auth_post_(request): sess = define.get_weasyl_session() # Only render page if the session exists //and// the password has # been authenticated (we have a UserID stored in the session) if not sess.additional_data or '2fa_pwd_auth_userid' not in sess.additional_data: return Response(define.errorpage(request.userid, errorcode.permission)) tfa_userid = sess.additional_data['2fa_pwd_auth_userid'] session_life = arrow.now().timestamp - sess.additional_data['2fa_pwd_auth_timestamp'] if session_life > 300: # Maximum secondary authentication time: 5 minutes _cleanup_2fa_session() return Response(define.errorpage( request.userid, errorcode.error_messages['TwoFactorAuthenticationAuthenticationTimeout'], [["Sign In", "/signin"], ["Return to the Home Page", "/"]] )) elif two_factor_auth.verify(tfa_userid, request.params["tfaresponse"]): # 2FA passed, so login and cleanup. _cleanup_2fa_session() login.signin(request, tfa_userid, ip_address=request.client_addr, user_agent=request.user_agent) ref = request.params["referer"] or "/" # User is out of recovery codes, so force-deactivate 2FA if two_factor_auth.get_number_of_recovery_codes(tfa_userid) == 0: two_factor_auth.force_deactivate(tfa_userid) return Response(define.errorpage( tfa_userid, errorcode.error_messages['TwoFactorAuthenticationZeroRecoveryCodesRemaining'], [["2FA Dashboard", "/control/2fa/status"], ["Return to the Home Page", "/"]] )) # Return to the target page, restricting to the path portion of 'ref' per urlparse. raise HTTPSeeOther(location=urlparse.urlparse(ref).path) elif sess.additional_data['2fa_pwd_auth_attempts'] >= 5: # Hinder brute-forcing the 2FA token or recovery code by enforcing an upper-bound on 2FA auth attempts. _cleanup_2fa_session() return Response(define.errorpage( request.userid, errorcode.error_messages['TwoFactorAuthenticationAuthenticationAttemptsExceeded'], [["Sign In", "/signin"], ["Return to the Home Page", "/"]] )) else: # Log the failed authentication attempt to the session and save sess.additional_data['2fa_pwd_auth_attempts'] += 1 sess.save = True # 2FA failed; redirect to 2FA input page & inform user that authentication failed. return Response(define.webpage( request.userid, "etc/signin_2fa_auth.html", [define.get_display_name(tfa_userid), request.params["referer"], two_factor_auth.get_number_of_recovery_codes(tfa_userid), "2fa"], title="Sign In - 2FA"))
def invalidate_other_sessions(userid): """ Invalidate all HTTP sessions for `userid` except for the current session. Useful as a security precaution, such as if a user changes their password, or enables 2FA. Parameters: userid: The userid for the account to clear sessions from. Returns: Nothing. """ sess = d.get_weasyl_session() d.engine.execute(""" DELETE FROM sessions WHERE userid = %(userid)s AND sessionid != %(currentsession)s """, userid=userid, currentsession=sess.sessionid)
def tfa_generate_recovery_codes_verify_password_post_(request): userid, status = login.authenticate_bcrypt(define.get_display_name( request.userid), request.params['password'], session=False) # 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 = define.get_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 signin_2fa_auth_get_(request): sess = define.get_weasyl_session() # Only render page if the password has been authenticated (we have a UserID stored in the session) if '2fa_pwd_auth_userid' not in sess.additional_data: return Response(define.errorpage(request.userid, errorcode.permission)) tfa_userid = sess.additional_data['2fa_pwd_auth_userid'] # Maximum secondary authentication time: 5 minutes session_life = arrow.now().timestamp - sess.additional_data['2fa_pwd_auth_timestamp'] if session_life > 300: _cleanup_2fa_session() return Response(define.errorpage( request.userid, errorcode.error_messages['TwoFactorAuthenticationAuthenticationTimeout'], [["Sign In", "/signin"], ["Return to the Home Page", "/"]])) else: ref = request.params["referer"] if "referer" in request.params else "/" return Response(define.webpage( request.userid, "etc/signin_2fa_auth.html", [define.get_display_name(tfa_userid), ref, two_factor_auth.get_number_of_recovery_codes(tfa_userid), None], title="Sign In - 2FA"))
def _get_recovery_codes_from_session(): sess = define.get_weasyl_session() if '2fa_recovery_codes' in sess.additional_data: return sess.additional_data['2fa_recovery_codes'] else: return None
def _set_recovery_codes_on_session(recovery_codes): sess = define.get_weasyl_session() sess.additional_data['2fa_recovery_codes'] = recovery_codes sess.additional_data['2fa_recovery_codes_timestamp'] = arrow.now( ).timestamp sess.save = True
def _get_totp_code_from_session(): sess = define.get_weasyl_session() return sess.additional_data['2fa_totp_code']
def _set_totp_code_on_session(totp_code): sess = define.get_weasyl_session() sess.additional_data['2fa_totp_code'] = totp_code sess.save = True
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 _set_recovery_codes_on_session(recovery_codes): sess = define.get_weasyl_session() sess.additional_data['2fa_recovery_codes'] = recovery_codes sess.additional_data['2fa_recovery_codes_timestamp'] = arrow.now().timestamp sess.save = True