def test_store_recovery_codes(): user_id = db_utils.create_user() valid_code_string = "01234567890123456789,02234567890123456789,03234567890123456789,04234567890123456789,05234567890123456789,06234567890123456789,07234567890123456789,08234567890123456789,09234567890123456789,10234567890123456789" _insert_recovery_code(user_id) # store_recovery_codes() will not accept a string of codes where the total code count is not 10 invalid_codes = valid_code_string.split(',').pop() assert not tfa.store_recovery_codes(user_id, ','.join(invalid_codes)) # store_recovery_codes() will not accept a string of codes when the code length is not tfa.LENGTH_RECOVERY_CODE invalid_codes = "01,02,03,04,05,06,07,08,09,10" assert not tfa.store_recovery_codes(user_id, invalid_codes) # When a correct code list is provided, the codes will be stored successfully in the database assert tfa.store_recovery_codes(user_id, valid_code_string) # Extract the current hashed recovery codes query = d.engine.execute(""" SELECT recovery_code_hash FROM twofa_recovery_codes WHERE userid = %(userid)s """, userid=user_id).fetchall() # Ensure that the recovery codes can be hashed to the corresponding bcrypt hash valid_code_list = valid_code_string.split(',') for row in query: code_status = False for code in valid_code_list: if bcrypt.checkpw(code.encode('utf-8'), row['recovery_code_hash'].encode('utf-8')): # If the code matches the hash, then the recovery code stored successfully code_status = True break # The code must be valid assert code_status
def tfa_generate_recovery_codes_post_(request): # Extract parameters from the form verify_checkbox = 'verify' in request.params tfaresponse = request.params['tfaresponse'] tfarecoverycodes = _get_recovery_codes_from_session() # Does the user want to save the new recovery codes? if verify_checkbox: if tfa.verify(request.userid, tfaresponse, consume_recovery_code=False): if tfa.store_recovery_codes(request.userid, tfarecoverycodes): # Clean up the stored session variables _cleanup_session() # Successfuly stored new recovery codes. raise HTTPSeeOther(location="/control/2fa/status") else: # Recovery code string was corrupted or otherwise altered. raise WeasylError("Unexpected") else: return Response( define.webpage( request.userid, "control/2fa/generate_recovery_codes.html", [tfarecoverycodes.split(','), "2fa"], title="Generate Recovery Codes: Save New Recovery Codes")) elif not verify_checkbox: return Response( define.webpage( request.userid, "control/2fa/generate_recovery_codes.html", [tfarecoverycodes.split(','), "verify"], title="Generate Recovery Codes: Save New Recovery Codes"))
def tfa_init_verify_post_(request): # Extract parameters from the form verify_checkbox = 'verify' in request.params tfasecret = _get_totp_code_from_session() tfarecoverycodes = _get_recovery_codes_from_session() # Does the user want to proceed with enabling 2FA? if verify_checkbox and tfa.store_recovery_codes(request.userid, tfarecoverycodes): # Strip any spaces from the TOTP code (some authenticators display the digits like '123 456') tfaresponse = request.params['tfaresponse'].replace(' ', '') # TOTP+2FA Secret validates (activate & redirect to status page) if tfa.activate(request.userid, tfasecret, tfaresponse): # Invalidate all other login sessions invalidate_other_sessions(request.userid) # Clean up the stored session variables _cleanup_session() raise HTTPSeeOther(location="/control/2fa/status") # TOTP+2FA Secret did not validate else: return Response( define.webpage(request.userid, "control/2fa/init_verify.html", [tfarecoverycodes.split(','), "2fa"], title="Enable 2FA: Final Step")) # The user didn't check the verification checkbox (despite HTML5's client-side check); regenerate codes & redisplay elif not verify_checkbox: return Response( define.webpage(request.userid, "control/2fa/init_verify.html", [tfarecoverycodes.split(','), "verify"], title="Enable 2FA: Final Step"))
def test_2fa_changes_token(app): user = db_utils.create_user(username='******', password='******') resp = app.get('/') csrf = resp.html.find('html')['data-csrf-token'] assert tfa.store_recovery_codes(user, ','.join(tfa.generate_recovery_codes())) tfa_secret = pyotp.random_base32() totp = pyotp.TOTP(tfa_secret) tfa_response = totp.now() assert tfa.activate(user, tfa_secret, tfa_response) old_cookie = app.cookies['WZL'] resp = app.post('/signin', { 'token': csrf, 'username': '******', 'password': '******' }) new_csrf = resp.html.find('html')['data-csrf-token'] assert app.cookies['WZL'] != old_cookie assert new_csrf != csrf assert not d.engine.scalar( "SELECT EXISTS (SELECT 0 FROM sessions WHERE userid = %(user)s)", user=user) assert d.engine.scalar( "SELECT EXISTS (SELECT 0 FROM sessions WHERE additional_data->'2fa_pwd_auth_userid' = %(user)s::text)", user=user)
def tfa_generate_recovery_codes_post_(request): # Extract parameters from the form verify_checkbox = 'verify' in request.params tfaresponse = request.params['tfaresponse'] tfarecoverycodes = _get_recovery_codes_from_session() # Does the user want to save the new recovery codes? if verify_checkbox: if tfa.verify(request.userid, tfaresponse, consume_recovery_code=False): if tfa.store_recovery_codes(request.userid, tfarecoverycodes): # Clean up the stored session variables _cleanup_session() # Successfuly stored new recovery codes. raise HTTPSeeOther(location="/control/2fa/status") else: # Recovery code string was corrupted or otherwise altered. raise WeasylError("Unexpected") else: return Response(define.webpage(request.userid, "control/2fa/generate_recovery_codes.html", [ tfarecoverycodes.split(','), "2fa" ], title="Generate Recovery Codes: Save New Recovery Codes")) elif not verify_checkbox: return Response(define.webpage(request.userid, "control/2fa/generate_recovery_codes.html", [ tfarecoverycodes.split(','), "verify" ], title="Generate Recovery Codes: Save New Recovery Codes"))
def tfa_init_verify_post_(request): # Extract parameters from the form verify_checkbox = 'verify' in request.params tfasecret = _get_totp_code_from_session() tfaresponse = request.params['tfaresponse'] tfarecoverycodes = _get_recovery_codes_from_session() # Does the user want to proceed with enabling 2FA? if verify_checkbox and tfa.store_recovery_codes(request.userid, tfarecoverycodes): # Strip any spaces from the TOTP code (some authenticators display the digits like '123 456') tfaresponse = request.params['tfaresponse'].replace(' ', '') # TOTP+2FA Secret validates (activate & redirect to status page) if tfa.activate(request.userid, tfasecret, tfaresponse): # Invalidate all other login sessions invalidate_other_sessions(request.userid) # Clean up the stored session variables _cleanup_session() raise HTTPSeeOther(location="/control/2fa/status") # TOTP+2FA Secret did not validate else: return Response(define.webpage(request.userid, "control/2fa/init_verify.html", [ tfarecoverycodes.split(','), "2fa" ], title="Enable 2FA: Final Step")) # The user didn't check the verification checkbox (despite HTML5's client-side check); regenerate codes & redisplay elif not verify_checkbox: return Response(define.webpage(request.userid, "control/2fa/init_verify.html", [ tfarecoverycodes.split(','), "verify" ], title="Enable 2FA: Final Step"))
def test_store_recovery_codes(): user_id = db_utils.create_user() valid_code_string = "01234567890123456789,02234567890123456789,03234567890123456789,04234567890123456789,05234567890123456789,06234567890123456789,07234567890123456789,08234567890123456789,09234567890123456789,10234567890123456789" _insert_recovery_code(user_id) # store_recovery_codes() will not accept a string of codes where the total code count is not 10 invalid_codes = valid_code_string.split(',').pop() assert not tfa.store_recovery_codes(user_id, ','.join(invalid_codes)) # store_recovery_codes() will not accept a string of codes when the code length is not tfa.LENGTH_RECOVERY_CODE invalid_codes = "01,02,03,04,05,06,07,08,09,10" assert not tfa.store_recovery_codes(user_id, invalid_codes) # When a correct code list is provided, the codes will be stored successfully in the database assert tfa.store_recovery_codes(user_id, valid_code_string) # Extract the current hashed recovery codes query = d.engine.execute(""" SELECT recovery_code_hash FROM twofa_recovery_codes WHERE userid = %(userid)s """, userid=user_id).fetchall() # Ensure that the recovery codes can be hashed to the corresponding bcrypt hash valid_code_list = valid_code_string.split(',') for row in query: code_status = False for code in valid_code_list: if bcrypt.checkpw(code.encode('utf-8'), row['recovery_code_hash'].encode('utf-8')): # If the code matches the hash, then the recovery code stored successfully code_status = True break # The code must be valid assert code_status
def test_2fa_changes_token(app): user = db_utils.create_user(username='******', password='******') resp = app.get('/') csrf = resp.html.find('html')['data-csrf-token'] assert tfa.store_recovery_codes(user, ','.join(tfa.generate_recovery_codes())) tfa_secret = pyotp.random_base32() totp = pyotp.TOTP(tfa_secret) tfa_response = totp.now() assert tfa.activate(user, tfa_secret, tfa_response) old_cookie = app.cookies['WZL'] resp = app.post('/signin', {'token': csrf, 'username': '******', 'password': '******'}) new_csrf = resp.html.find('html')['data-csrf-token'] assert app.cookies['WZL'] != old_cookie assert new_csrf != csrf assert not d.engine.scalar("SELECT EXISTS (SELECT 0 FROM sessions WHERE userid = %(user)s)", user=user) assert d.engine.scalar("SELECT EXISTS (SELECT 0 FROM sessions WHERE additional_data->'2fa_pwd_auth_userid' = %(user)s::text)", user=user)