def test_tf_select(app, client, get_message): # Test basic select mechanism when more than one 2FA has been setup wankeys = reg_2_keys(client) # add a webauthn 2FA key (authenticates) sms_sender = setup_tf(client) logout(client) # since we have 2 2FA methods configured - we should get the tf-select form response = client.post( "/login", data=dict(email="*****@*****.**", password="******"), follow_redirects=True, ) assert b"Select Two Factor Method" in response.data response = client.post("/tf-select", data=dict(which="webauthn"), follow_redirects=True) assert b"Use Your WebAuthn Security Key as a Second Factor" in response.data response = wan_signin(client, "*****@*****.**", wankeys["secondary"]["signin"]) assert not tf_in_session(get_session(response)) # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 # now do other 2FA logout(client) response = client.post( "/login", data=dict(email="*****@*****.**", password="******"), follow_redirects=True, ) assert b"Select Two Factor Method" in response.data response = client.post("/tf-select", data=dict(which="sms"), follow_redirects=True) assert b"Please enter your authentication code generated via: sms" in response.data code = sms_sender.messages[0].split()[-1] response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"Your token has been confirmed" in response.data assert not tf_in_session(get_session(response)) # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 assert not tf_in_session(get_existing_session(client))
def test_setup_bad_phone(app, client): data = dict(email="*****@*****.**", password="******") response = client.post("/login", data=data, follow_redirects=True) message = b"Two-factor authentication adds an extra layer of security" assert message in response.data sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="555-1212") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"Phone number not valid" in response.data assert sms_sender.get_count() == 0 # Now setup good phone response = client.post( "/tf-setup", data=dict(setup="sms", phone="650-555-1212"), follow_redirects=True ) assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] # shouldn't get authenticator stuff when setting up SMS assert b"data:image/png;base64," not in response.data response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"Your token has been confirmed" in response.data assert not tf_in_session(get_session(response)) headers = {"Accept": "application/json", "Content-Type": "application/json"} response = client.get("/tf-setup", headers=headers) # N.B. right now for tfa - we don't canonicalize phone number (since user # never has to type it in). assert response.json["response"]["tf_phone_number"] == "650-555-1212"
def test_evil_validate(app, client): """ Test logged in, and randomly try to validate a token """ signalled_identity = [] @identity_changed.connect_via(app) def on_identity_changed(app, identity): signalled_identity.append(identity.id) response = authenticate(client, "*****@*****.**") session = get_session(response) assert "tf_state" not in session with app.app_context(): user = app.security.datastore.find_user(email="*****@*****.**") assert signalled_identity[0] == user.fs_uniquifier del signalled_identity[:] # try to validate response = client.post("/tf-validate", data=dict(code="?"), follow_redirects=True) # This should log us out since it thinks we are evil assert not signalled_identity[0] del signalled_identity[:]
def test_admin_setup_reset(app, client, get_message): # Verify can use administrative datastore method to setup SMS # and that administrative reset removes access. sms_sender = SmsSenderFactory.createSender("test") data = dict(email="*****@*****.**", password="******") response = client.post( "/login", json=data, headers={"Content-Type": "application/json"} ) assert response.json["response"]["tf_required"] # we shouldn't be logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 302 assert response.location == "http://localhost/login?next=%2Fprofile" # Use admin to setup gene's SMS/phone. with app.app_context(): user = app.security.datastore.find_user(email="*****@*****.**") totp_secret = app.security._totp_factory.generate_totp_secret() app.security.datastore.tf_set(user, "sms", totp_secret, phone="+442083661177") app.security.datastore.commit() response = authenticate(client, "*****@*****.**") session = get_session(response) assert session["tf_state"] == "ready" # Grab code that was sent assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"Your token has been confirmed" in response.data # verify we are logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 # logout logout(client) # use administrative reset method with app.app_context(): user = app.security.datastore.find_user(email="*****@*****.**") app.security.datastore.reset_user_access(user) app.security.datastore.commit() data = dict(email="*****@*****.**", password="******") response = client.post( "/login", json=data, headers={"Content-Type": "application/json"} ) assert response.json["response"]["tf_required"] assert response.json["response"]["tf_state"] == "setup_from_login" # we shouldn't be logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 302
def test_login_csrf_json(app, client): app.config["WTF_CSRF_ENABLED"] = True with mp_validate_csrf() as mp: auth_token, csrf_token = json_login(client) assert auth_token assert csrf_token # Should be just one call to validate - since CSRFProtect not enabled. assert mp.success == 1 and mp.failure == 0 response = json_logout(client) session = get_session(response) assert "csrf_token" not in session
def test_datastore(app, client): # Test that user record is properly set after proper 2FA setup. sms_sender = SmsSenderFactory.createSender("test") data = dict(email="*****@*****.**", password="******") response = client.post("/login", json=data, headers={"Content-Type": "application/json"}) assert response.json["meta"]["code"] == 200 session = get_session(response) assert session["tf_state"] == "setup_from_login" # setup data = dict(setup="sms", phone="+442083661177") response = client.post("/tf-setup", json=data, headers={"Content-Type": "application/json"}) assert sms_sender.get_count() == 1 session = get_session(response) assert session["tf_state"] == "validating_profile" assert session["tf_primary_method"] == "sms" code = sms_sender.messages[0].split()[-1] # submit token and show appropriate response response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"Your token has been confirmed" in response.data session = get_session(response) # Verify that successful login clears session info assert not tf_in_session(session) with app.app_context(): user = app.security.datastore.find_user(email="*****@*****.**") assert user.tf_primary_method == "sms" assert user.tf_phone_number == "+442083661177" assert "enckey" in user.tf_totp_secret
def test_opt_out_json(app, client): headers = {"Accept": "application/json", "Content-Type": "application/json"} tf_authenticate(app, client) response = client.get("tf-setup", headers=headers) assert "disable" in response.json["response"]["tf_available_methods"] response = client.post("tf-setup", json=dict(setup="disable"), headers=headers) assert response.status_code == 200 logout(client) # Should be able to log in with just user/pass response = authenticate(client, "*****@*****.**") session = get_session(response) assert "tf_state" not in session # verify logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200
def test_change_invalidates_session(app, client): # Make sure that if we change our password - prior sessions are invalidated. # changing password effectively re-logs in user - verify the signal auths = [] @user_authenticated.connect_via(app) def authned(myapp, user, **extra_args): auths.append((user.email, extra_args["authn_via"])) # No remember cookie since that also be reset and auto-login. data = dict(email="*****@*****.**", password="******", remember="") response = client.post("/login", data=data) sess = get_session(response) cur_user_id = sess.get("_user_id", sess.get("user_id")) response = client.post( "/change", data={ "password": "******", "new_password": "******", "new_password_confirm": "new strong password", }, follow_redirects=True, ) # First auth was the initial login above - second should be from /change assert auths[1][0] == "*****@*****.**" assert "change" in auths[1][1] # Should have received a new session cookie - so should still be logged in response = client.get("/profile", follow_redirects=True) assert b"Profile Page" in response.data # Now use old session - shouldn't work. with client.session_transaction() as oldsess: oldsess["_user_id"] = cur_user_id oldsess["user_id"] = cur_user_id # try to access protected endpoint - shouldn't work response = client.get("/profile") assert response.status_code == 302 assert response.headers[ "Location"] == "http://localhost/login?next=%2Fprofile"
def test_totp_secret_generation(app, client): """ Test the totp secret generation upon changing method to make sure it stays the same after the process is completed """ # Properly log in jill for this test signalled_identity = [] @identity_changed.connect_via(app) def on_identity_changed(app, identity): signalled_identity.append(identity.id) response = authenticate(client, "*****@*****.**") session = get_session(response) assert "tf_state" not in session with app.app_context(): user = app.security.datastore.find_user(email="*****@*****.**") assert signalled_identity[0] == user.fs_uniquifier del signalled_identity[:] sms_sender = SmsSenderFactory.createSender("test") # Select sms method but do not send a phone number just yet (regenerates secret) data = dict(setup="sms") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"To Which Phone Number Should We Send Code To" in response.data # Retrieve the currently generated totp secret for later comparison session = get_session(response) if "tf_totp_secret" in session: generated_secret = session["tf_totp_secret"] else: with app.app_context(): user = app.security.datastore.find_user(email="*****@*****.**") generated_secret = user.tf_totp_secret assert "enckey" in generated_secret # Send a new phone number in the second step, method remains unchanged data = dict(setup="sms", phone="+442083661188") response = client.post("/tf-setup", data=data, follow_redirects=True) assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] # Validate token - this should complete 2FA setup response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"You successfully changed" in response.data # Retrieve the final totp secret and make sure it matches the previous one with app.app_context(): user = app.security.datastore.find_user(email="*****@*****.**") assert generated_secret == user.tf_totp_secret # Finally opt back out and check that tf_totp_secret is None data = dict(setup="disable") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"You successfully disabled two factor authorization." in response.data with app.app_context(): user = app.security.datastore.find_user(email="*****@*****.**") assert user.tf_totp_secret is None # Log out logout(client) assert not signalled_identity[0] del signalled_identity[:]
def test_opt_in(app, client): """ Test entire lifecycle of user not having 2FA - setting it up, then deciding to turn it back off All using forms based API """ signalled_identity = [] @identity_changed.connect_via(app) def on_identity_changed(app, identity): signalled_identity.append(identity.id) response = authenticate(client, "*****@*****.**") session = get_session(response) assert "tf_state" not in session with app.app_context(): user = app.security.datastore.find_user(email="*****@*****.**") assert signalled_identity[0] == user.fs_uniquifier del signalled_identity[:] # opt-in for SMS 2FA sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="+442083661177") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"To Which Phone Number Should We Send Code To" in response.data assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] # Validate token - this should complete 2FA setup response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"You successfully changed" in response.data # Upon completion, session cookie shouldnt have any two factor stuff in it. session = get_session(response) assert not tf_in_session(session) # Log out logout(client) assert not signalled_identity[0] del signalled_identity[:] # Login now should require 2FA with sms sms_sender = SmsSenderFactory.createSender("test") response = authenticate(client, "*****@*****.**") session = get_session(response) assert session["tf_state"] == "ready" assert len(signalled_identity) == 0 assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"Your token has been confirmed" in response.data # Verify now logged in with app.app_context(): user = app.security.datastore.find_user(email="*****@*****.**") assert signalled_identity[0] == user.fs_uniquifier del signalled_identity[:] # Now opt back out. data = dict(setup="disable") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"You successfully disabled two factor authorization." in response.data # Log out logout(client) assert not signalled_identity[0] del signalled_identity[:] # Should be able to log in with just user/pass response = authenticate(client, "*****@*****.**") session = get_session(response) assert "tf_state" not in session with app.app_context(): user = app.security.datastore.find_user(email="*****@*****.**") assert signalled_identity[0] == user.fs_uniquifier
def test_json(app, client): """ Test login/setup using JSON. """ headers = {"Accept": "application/json", "Content-Type": "application/json"} # Login with someone already setup. sms_sender = SmsSenderFactory.createSender("test") data = dict(email="*****@*****.**", password="******") response = client.post("/login", json=data, headers=headers) assert response.status_code == 200 assert response.json["response"]["tf_required"] assert response.json["response"]["tf_state"] == "ready" assert response.json["response"]["tf_primary_method"] == "sms" # Verify SMS sent assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] response = client.post("/tf-validate", json=dict(code=code), headers=headers) assert response.status_code == 200 # verify logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 logout(client) # Test that user not yet setup for 2FA gets correct response. data = dict(email="*****@*****.**", password="******") response = client.post("/login", json=data, headers=headers) assert response.json["response"]["tf_required"] assert response.json["response"]["tf_state"] == "setup_from_login" # Start setup process. response = client.get("/tf-setup", headers=headers) assert response.json["response"]["tf_required"] assert "sms" in response.json["response"]["tf_available_methods"] # Now try to setup data = dict(setup="sms", phone="+442083661177") response = client.post("/tf-setup", json=data, headers=headers) assert response.status_code == 200 assert response.json["response"]["tf_state"] == "validating_profile" assert response.json["response"]["tf_primary_method"] == "sms" code = sms_sender.messages[0].split()[-1] response = client.post("/tf-validate", json=dict(code=code), headers=headers) assert response.status_code == 200 assert "csrf_token" in response.json["response"] assert response.json["response"]["user"]["email"] == "*****@*****.**" logout(client) # Verify tf is now setup and can directly get code data = dict(email="*****@*****.**", password="******") response = client.post("/login", json=data, headers=headers) assert response.json["response"]["tf_required"] assert response.json["response"]["tf_state"] == "ready" code = sms_sender.messages[0].split()[-1] response = client.post("/tf-validate", json=dict(code=code), headers=headers) assert response.status_code == 200 # verify logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 # tf-setup should provide existing info response = client.get("/tf-setup", headers=headers) assert response.json["response"]["tf_required"] assert "sms" in response.json["response"]["tf_available_methods"] assert "disable" not in response.json["response"]["tf_available_methods"] assert response.json["response"]["tf_primary_method"] == "sms" assert response.json["response"]["tf_phone_number"] == "+442083661177" assert not tf_in_session(get_session(response))
def test_two_factor_flag(app, client): # trying to verify code without going through two-factor # first login function wrong_code = b"000000" response = client.post( "/tf-validate", data=dict(code=wrong_code), follow_redirects=True ) message = b"You currently do not have permissions to access this page" assert message in response.data # Test login using invalid email data = dict(email="*****@*****.**", password="******") response = client.post("/login", data=data, follow_redirects=True) assert b"Specified user does not exist" in response.data response = client.post( "/login", json=data, headers={"Content-Type": "application/json"}, follow_redirects=True, ) assert b"Specified user does not exist" in response.data # Test login using valid email and invalid password data = dict(email="*****@*****.**", password="******") response = client.post("/login", data=data, follow_redirects=True) assert b"Invalid password" in response.data response = client.post( "/login", json=data, headers={"Content-Type": "application/json"}, follow_redirects=True, ) assert b"Invalid password" in response.data # Test two-factor authentication first login data = dict(email="*****@*****.**", password="******") response = client.post("/login", data=data, follow_redirects=True) message = b"Two-factor authentication adds an extra layer of security" assert message in response.data response = client.post( "/tf-setup", data=dict(setup="not_a_method"), follow_redirects=True ) assert b"Marked method is not valid" in response.data session = get_session(response) assert session["tf_state"] == "setup_from_login" # try non-existing setup on setup page (using json) data = dict(setup="not_a_method") response = client.post( "/tf-setup", json=data, headers={"Content-Type": "application/json"}, follow_redirects=True, ) assert response.status_code == 400 assert ( response.json["response"]["errors"]["setup"][0] == "Marked method is not valid" ) data = dict(setup="email") response = client.post( "/tf-setup", json=data, headers={"Content-Type": "application/json"}, follow_redirects=True, ) # Test for sms in process of valid login sms_sender = SmsSenderFactory.createSender("test") data = dict(email="*****@*****.**", password="******") response = client.post( "/login", json=data, headers={"Content-Type": "application/json"}, follow_redirects=True, ) assert b'"code": 200' in response.data assert sms_sender.get_count() == 1 session = get_session(response) assert session["tf_state"] == "ready" code = sms_sender.messages[0].split()[-1] # submit bad token to two_factor_token_validation response = client.post("/tf-validate", data=dict(code=wrong_code)) assert b"Invalid Token" in response.data # sumbit right token and show appropriate response response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"Your token has been confirmed" in response.data # Upon completion, session cookie shouldnt have any two factor stuff in it. assert not tf_in_session(get_session(response)) # Test change two_factor view to from sms to mail with app.mail.record_messages() as outbox: setup_data = dict(setup="email") response = client.post("/tf-setup", data=setup_data, follow_redirects=True) msg = b"To complete logging in, please enter the code sent to your mail" assert msg in response.data # Fetch token validate form response = client.get("/tf-validate") assert response.status_code == 200 assert b'name="code"' in response.data code = outbox[0].body.split()[-1] # sumbit right token and show appropriate response response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"You successfully changed your two-factor method" in response.data # Test change two_factor password confirmation view to authenticator # Setup authenticator setup_data = dict(setup="authenticator") response = client.post("/tf-setup", data=setup_data, follow_redirects=True) assert b"Open an authenticator app on your device" in response.data # verify png QRcode is present assert b"data:image/svg+xml;base64," in response.data # parse out key rd = response.data.decode("utf-8") matcher = re.match(r".*((?:\S{4}-){7}\S{4}).*", rd, re.DOTALL) totp_secret = matcher.group(1) # Generate token from passed totp_secret and confirm setup totp = TOTP(totp_secret) code = totp.generate().token response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"You successfully changed your two-factor method" in response.data logout(client) # Test login with remember_token assert "remember_token" not in [c.name for c in client.cookie_jar] data = dict(email="*****@*****.**", password="******", remember=True) response = client.post( "/login", json=data, headers={"Content-Type": "application/json"}, follow_redirects=True, ) # Generate token from passed totp_secret code = totp.generate().token response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"Your token has been confirmed" in response.data # Verify that the remember token is properly set found = False for cookie in client.cookie_jar: if cookie.name == "remember_token": found = True assert cookie.path == "/" assert found response = logout(client) # Verify that logout clears session info assert not tf_in_session(get_session(response)) # Test two-factor authentication first login data = dict(email="*****@*****.**", password="******") response = client.post("/login", data=data, follow_redirects=True) message = b"Two-factor authentication adds an extra layer of security" assert message in response.data # check availability of qrcode when this option is not picked assert b"data:image/png;base64," not in response.data # check availability of qrcode page when this option is picked setup_data = dict(setup="authenticator") response = client.post("/tf-setup", data=setup_data, follow_redirects=True) assert b"Open an authenticator app on your device" in response.data assert b"data:image/svg+xml;base64," in response.data # check appearence of setup page when sms picked and phone number entered sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="+442083661177") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"To Which Phone Number Should We Send Code To" in response.data assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"Your token has been confirmed" in response.data assert not tf_in_session(get_session(response)) logout(client) # check when two_factor_rescue function should not appear rescue_data_json = dict(help_setup="lost_device") response = client.post( "/tf-rescue", json=rescue_data_json, headers={"Content-Type": "application/json"}, ) assert b'"code": 400' in response.data # check when two_factor_rescue function should appear data = dict(email="*****@*****.**", password="******") response = client.post("/login", data=data, follow_redirects=True) assert b"Please enter your authentication code" in response.data rescue_data = dict(help_setup="lost_device") response = client.post("/tf-rescue", data=rescue_data, follow_redirects=True) message = b"The code for authentication was sent to your email address" assert message in response.data rescue_data = dict(help_setup="no_mail_access") response = client.post("/tf-rescue", data=rescue_data, follow_redirects=True) message = b"A mail was sent to us in order to reset your application account" assert message in response.data