def tf_authenticate(app, client, json=False, validate=True, remember=False): """Login/Authenticate using two factor. This is the equivalent of utils:authenticate """ prev_sms = app.config["SECURITY_SMS_SERVICE"] app.config["SECURITY_SMS_SERVICE"] = "test" sms_sender = SmsSenderFactory.createSender("test") json_data = dict(email="*****@*****.**", password="******", remember=remember) response = client.post( "/login", json=json_data, headers={"Content-Type": "application/json"} ) assert b'"code": 200' in response.data app.config["SECURITY_SMS_SERVICE"] = prev_sms if validate: code = sms_sender.messages[0].split()[-1] if json: response = client.post( "/tf-validate", json=dict(code=code), headers={"Content-Type": "application/json"}, ) assert b'"code": 200' in response.data return response.json["response"].get("tf_validity_token", None) else: response = client.post( "/tf-validate", data=dict(code=code), follow_redirects=True ) assert response.status_code == 200
def test_recoverable(app, client, get_message): # make sure 'forgot password' doesn't bypass 2FA. # '*****@*****.**' already setup for SMS rtokens = [] sms_sender = SmsSenderFactory.createSender("test") @reset_password_instructions_sent.connect_via(app) def on_instructions_sent(sapp, **kwargs): rtokens.append(kwargs["token"]) client.post("/reset", data=dict(email="*****@*****.**"), follow_redirects=True) response = client.post( "/reset/" + rtokens[0], data={"password": "******", "password_confirm": "awesome sunset"}, follow_redirects=True, ) # Should have redirected us to the 2FA login page assert b"Please enter your authentication code" in response.data # we shouldn't be logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 302 # 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
def test_json(app, client): """ Test all endpoints using JSON. (eventually) """ # Test that user not yet setup for 2FA gets correct response. 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" # Login with someone already setup. sms_sender = SmsSenderFactory.createSender("test") data = dict(email="*****@*****.**", password="******") response = client.post( "/login", json=data, headers={"Content-Type": "application/json"} ) 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={"Content-Type": "application/json"}, ) assert response.status_code == 200
def test_no_opt_out(app, client): # Test if 2FA required, can't opt-out. sms_sender = SmsSenderFactory.createSender("test") response = client.post( "/login", data=dict(email="*****@*****.**", password="******"), follow_redirects=True, ) assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] # submit 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 response = client.post( "/tf-confirm", data=dict(password="******"), follow_redirects=True ) assert b"You successfully confirmed password" in response.data response = client.get("/tf-setup", follow_redirects=True) assert b"Disable two factor" not in response.data # Try to opt-out data = dict(setup="disable") response = client.post("/tf-setup", data=data, follow_redirects=True) assert response.status_code == 200 assert b"Marked method is not valid" in response.data
def test_setup_timeout(app, client, get_message): # Test setup timeout us_authenticate(client) headers = { "Accept": "application/json", "Content-Type": "application/json" } sms_sender = SmsSenderFactory.createSender("test") response = client.post( "us-setup", json=dict(chosen_method="sms", phone="650-555-1212"), headers=headers, ) assert response.status_code == 200 state = response.json["response"]["state"] time.sleep(1) code = sms_sender.messages[0].split()[-1].strip(".") response = client.post("/us-setup/" + state, json=dict(code=code), headers=headers) assert response.status_code == 400 assert response.json["response"]["error"].encode("utf-8") == get_message( "US_SETUP_EXPIRED", within=app.config["SECURITY_US_SETUP_WITHIN"])
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_setup_new_totp(app, client, get_message): # us-setup has a 'generate new totp-secret' option # Verify that works (and existing codes no longer work) us_authenticate(client) headers = { "Accept": "application/json", "Content-Type": "application/json" } # Start by generating a good code sms_sender = SmsSenderFactory.createSender("test") response = client.post( "us-setup", json=dict(chosen_method="sms", phone="650-555-1212"), headers=headers, ) assert response.status_code == 200 code = sms_sender.messages[0].split()[-1].strip(".") # Now send correct value - this should generate new totp - so the previous 'code' # should no longer work sms_sender2 = SmsSenderFactory.createSender("test") response = client.post( "us-setup", json=dict(chosen_method="sms", phone="650-555-1212", new_totp_secret=True), headers=headers, ) assert response.status_code == 200 state = response.json["response"]["state"] # Use old code response = client.post("/us-setup/" + state, json=dict(code=code), headers=headers) assert response.status_code == 400 # Use new code code = sms_sender2.messages[0].split()[-1].strip(".") response = client.post("/us-setup/" + state, json=dict(code=code), headers=headers) assert response.status_code == 200 assert response.json["response"]["chosen_method"] == "sms" assert response.json["response"]["phone"] == "+16505551212"
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_tf_not(app, client, get_message): # Test basic two-factor - when first factor doesn't require second (e.g. SMS) # 1. sign in and setup TFA client.post( "/us-signin", data=dict(identity="*****@*****.**", passcode="password", remember=True), follow_redirects=True, ) sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="+442083661177") client.post("/tf-setup", data=data, follow_redirects=True) code = sms_sender.messages[0].split()[-1] client.post("/tf-validate", data=dict(code=code), follow_redirects=True) # 2. setup unified sign in with SMS response = client.post("us-setup", data=dict(chosen_method="sms", phone="650-555-1212")) matcher = re.match( r'.*<form action="([^\s]*)".*', response.data.decode("utf-8"), re.IGNORECASE | re.DOTALL, ) verify_url = matcher.group(1) code = sms_sender.messages[0].split()[-1].strip(".") response = client.post(verify_url, data=dict(code=code), follow_redirects=True) assert response.status_code == 200 assert get_message("US_SETUP_SUCCESSFUL") in response.data # 3. logout logout(client) # 4. sign in with SMS - should not require TFA client.post( "/us-send-code", data=dict(identity="*****@*****.**", chosen_method="sms"), follow_redirects=True, ) code = sms_sender.messages[0].split()[-1].strip(".") response = client.post( "/us-signin", data=dict(identity="6505551212", passcode=code), follow_redirects=True, ) assert response.status_code == 200 # assert "sms" in auths[1][1] # Verify authenticated response = client.get("/profile", follow_redirects=False) assert response.status_code == 200
def test_setup(app, client, get_message): us_authenticate(client) response = client.get("us-setup") assert all( i in response.data for i in [b"chosen_method-0", b"chosen_method-1", b"chosen_method-2"]) # test missing phone response = client.post("us-setup", data=dict(chosen_method="sms", phone="")) assert response.status_code == 200 assert get_message("PHONE_INVALID") in response.data # test invalid phone response = client.post("us-setup", data=dict(chosen_method="sms", phone="555-1212")) assert response.status_code == 200 assert get_message("PHONE_INVALID") in response.data sms_sender = SmsSenderFactory.createSender("test") response = client.post("us-setup", data=dict(chosen_method="sms", phone="650-555-1212")) assert response.status_code == 200 assert b"Submit Code" in response.data matcher = re.match( r'.*<form action="([^\s]*)".*', response.data.decode("utf-8"), re.IGNORECASE | re.DOTALL, ) verify_url = matcher.group(1) # Try invalid code response = client.post(verify_url, data=dict(code=12345), follow_redirects=True) assert get_message("INVALID_CODE") in response.data code = sms_sender.messages[0].split()[-1].strip(".") response = client.post(verify_url, data=dict(code=code), follow_redirects=True) assert response.status_code == 200 assert get_message("US_SETUP_SUCCESSFUL") in response.data
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 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] 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))
def test_tf(app, client, get_message): # Test basic two-factor - default for signing in with password. response = client.post( "/us-signin", data=dict(identity="*****@*****.**", passcode="password", remember=True), follow_redirects=True, ) assert response.status_code == 200 message = b"Two-factor authentication adds an extra layer of security" assert message in response.data assert b"Set up using SMS" in response.data 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 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
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_setup_json(app, client_nc, get_message): @us_profile_changed.connect_via(app) def pc(sender, user, method): assert method == "sms" assert user.us_phone_number == "+16505551212" token = us_authenticate(client_nc) headers = { "Authentication-Token": token, "Accept": "application/json", "Content-Type": "application/json", } response = client_nc.get("/us-setup", headers=headers) assert response.status_code == 200 assert response.json["response"]["methods"] == ["email", "sms"] sms_sender = SmsSenderFactory.createSender("test") response = client_nc.post( "us-setup", json=dict(chosen_method="sms", phone="650-555-1212"), headers=headers, ) assert response.status_code == 200 state = response.json["response"]["state"] assert state # send invalid code response = client_nc.post("/us-setup/" + state, json=dict(code=12344), headers=headers) assert response.status_code == 400 assert response.json["response"]["errors"]["code"][0].encode( "utf-8") == get_message("INVALID_CODE") code = sms_sender.messages[0].split()[-1].strip(".") response = client_nc.post("/us-setup/" + state, json=dict(code=code), headers=headers) assert response.status_code == 200 assert response.json["response"]["chosen_method"] == "sms" assert response.json["response"]["phone"] == "+16505551212" # now login with phone - send in different format than we set up with. headers = { "Accept": "application/json", "Content-Type": "application/json" } sms_sender = SmsSenderFactory.createSender("test") response = client_nc.post( "/us-send-code", json=dict(identity="6505551212", chosen_method="sms"), headers=headers, ) assert response.status_code == 200 code = sms_sender.messages[0].split()[-1].strip(".") response = client_nc.post( "/us-signin?include_auth_token", json=dict(identity="*****@*****.**", passcode=code), headers=headers, ) assert response.status_code == 200 assert "authentication_token" in response.json["response"]["user"]
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 # Jill is 4th user to be added in utils.py assert signalled_identity[0] == 4 del signalled_identity[:] # opt-in for SMS 2FA - but we haven't re-verified password sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="+442083661177") response = client.post("/tf-setup", data=data, follow_redirects=True) message = b"You currently do not have permissions to access this page" assert message in response.data # Confirm password - then opt-in password = "******" response = client.post( "/tf-confirm", data=dict(password=password), follow_redirects=True ) 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 assert signalled_identity[0] == 4 del signalled_identity[:] # Now opt back out. # as before must reconfirm password first response = client.get("/tf-setup", data=data, follow_redirects=True) message = b"You currently do not have permissions to access this page" assert message in response.data password = "******" client.post("/tf-confirm", data=dict(password=password), follow_redirects=True) 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 # Jill is 4th user to be added in utils.py assert signalled_identity[0] == 4
def test_simple_login(app, client, get_message): auths = [] @user_authenticated.connect_via(app) def authned(myapp, user, **extra_args): auths.append((user.email, extra_args["authn_via"])) # Test missing choice data = dict(identity="*****@*****.**") response = client.post("/us-send-code", data=data, follow_redirects=True) assert get_message("US_METHOD_NOT_AVAILABLE") in response.data # Test login using invalid email data = dict(identity="*****@*****.**", chosen_method="email") response = client.post("/us-send-code", data=data, follow_redirects=True) assert get_message("US_SPECIFY_IDENTITY") in response.data # test disabled account data = dict(identity="*****@*****.**", chosen_method="email") response = client.post("/us-send-code", data=data, follow_redirects=True) assert b"Code has been sent" not in response.data assert get_message("DISABLED_ACCOUNT") in response.data with capture_send_code_requests() as requests: with app.mail.record_messages() as outbox: response = client.post( "/us-send-code", data=dict(identity="*****@*****.**", chosen_method="email"), follow_redirects=True, ) assert response.status_code == 200 assert b"Sign In" in response.data assert len(requests) == 1 assert len(outbox) == 1 # try bad code response = client.post( "/us-signin", data=dict(identity="*****@*****.**", passcode="blahblah"), follow_redirects=True, ) assert get_message("INVALID_PASSWORD") in response.data # Correct code assert "remember_token" not in [c.name for c in client.cookie_jar] assert "session" not in [c.name for c in client.cookie_jar] response = client.post( "/us-signin", data=dict(identity="*****@*****.**", passcode=requests[0]["token"]), follow_redirects=False, ) assert "remember_token" not in [c.name for c in client.cookie_jar] assert "email" in auths[0][1] response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 logout(client) response = client.get("/profile", follow_redirects=False) assert "/login?next=%2Fprofile" in response.location # login via SMS sms_sender = SmsSenderFactory.createSender("test") set_phone(app) response = client.post( "/us-send-code", data=dict(identity="*****@*****.**", chosen_method="sms"), follow_redirects=True, ) assert response.status_code == 200 assert b"Sign In" in response.data code = sms_sender.messages[0].split()[-1].strip(".") response = client.post( "/us-signin", data=dict(identity="*****@*****.**", passcode=code, remember=True), follow_redirects=True, ) assert response.status_code == 200 assert "remember_token" in [c.name for c in client.cookie_jar] assert "sms" in auths[1][1] response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 logout(client) assert "remember_token" not in [c.name for c in client.cookie_jar]
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
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 # Jill is 4th user to be added in utils.py assert signalled_identity[0] == 4 del signalled_identity[:] # Confirm password sms_sender = SmsSenderFactory.createSender("test") password = "******" response = client.post( "/tf-confirm", data=dict(password=password), follow_redirects=True ) # 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 response = client.get("/tf-setup", data=data, follow_redirects=True) message = b"You currently do not have permissions to access this page" assert message in response.data password = "******" client.post("/tf-confirm", data=dict(password=password), follow_redirects=True) 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_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)) # try confirming password with a wrong one response = client.post("/tf-confirm", data=dict(password=""), follow_redirects=True) assert b"Password not provided" in response.data # try confirming password with a wrong one + json data = dict(password="******") response = client.post( "/tf-confirm", json=data, headers={"Content-Type": "application/json"}, follow_redirects=True, ) assert response.json["meta"]["code"] == 400 # Test change two_factor password confirmation view to mail password = "******" response = client.post( "/tf-confirm", data=dict(password=password), follow_redirects=True ) assert b"You successfully confirmed password" in response.data message = b"Two-factor authentication adds an extra layer of security" assert message in response.data # change method (from sms to mail) setup_data = dict(setup="email") testMail = TestMail() app.extensions["mail"] = testMail 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 = testMail.msg.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 google authenticator password = "******" response = client.post( "/tf-confirm", data=dict(password=password), follow_redirects=True ) assert b"You successfully confirmed password" in response.data message = b"Two-factor authentication adds an extra layer of security" assert message in response.data # Setup authenticator setup_data = dict(setup="authenticator") response = client.post("/tf-setup", data=setup_data, follow_redirects=True) assert b"Open your authenticator app on your device" in response.data # Now request code. We can't test the qrcode easily - but we can get the totp_secret # that goes into the qrcode and make sure that works mtf = Mock(wraps=app.security._totp_factory) app.security.totp_factory(mtf) qrcode_page_response = client.get( "/tf-qrcode", data=setup_data, follow_redirects=True ) assert mtf.get_totp_uri.call_count == 1 (username, totp_secret), _ = mtf.get_totp_uri.call_args assert username == "*****@*****.**" assert b"svg" in qrcode_page_response.data # Generate token from passed totp_secret and confirm setup code = app.security._totp_factory.generate_totp_password(totp_secret) 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 = app.security._totp_factory.generate_totp_password(totp_secret) 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 page when this option is not picked qrcode_page_response = client.get("/two_factor_qrcode/", follow_redirects=False) assert qrcode_page_response.status_code == 404 # 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 your authenticator app on your device" in response.data qrcode_page_response = client.get( "/tf-qrcode", data=setup_data, follow_redirects=True ) print(qrcode_page_response) assert b"svg" in qrcode_page_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" + b" to reset your application account" assert message in response.data
def test_simple_login_json(app, client_nc, get_message): auths = [] @user_authenticated.connect_via(app) def authned(myapp, user, **extra_args): auths.append((user.email, extra_args["authn_via"])) headers = { "Accept": "application/json", "Content-Type": "application/json" } with capture_flashes() as flashes: response = client_nc.get("/us-signin", headers=headers) assert (response.json["response"]["methods"] == app.config["SECURITY_US_ENABLED_METHODS"]) assert (response.json["response"]["identity_attributes"] == app.config["SECURITY_USER_IDENTITY_ATTRIBUTES"]) with capture_send_code_requests() as requests: with app.mail.record_messages() as outbox: response = client_nc.post( "/us-send-code", json=dict(identity="*****@*****.**", chosen_method="email"), headers=headers, follow_redirects=True, ) assert response.status_code == 200 assert "csrf_token" in response.json["response"] assert "user" not in response.json["response"] assert len(requests) == 1 assert len(outbox) == 1 # try bad code response = client_nc.post( "/us-signin", json=dict(identity="*****@*****.**", passcode="blahblah"), headers=headers, follow_redirects=True, ) assert response.status_code == 400 assert response.json["response"]["errors"]["passcode"][0].encode( "utf-8") == get_message("INVALID_PASSWORD") # Login successfully with code response = client_nc.post( "/us-signin?include_auth_token", json=dict(identity="*****@*****.**", passcode=requests[0]["token"]), headers=headers, follow_redirects=True, ) assert response.status_code == 200 assert "authentication_token" in response.json["response"]["user"] assert "email" in auths[0][1] logout(client_nc) response = client_nc.get("/profile", headers=headers, follow_redirects=False) assert response.status_code == 401 # login via SMS sms_sender = SmsSenderFactory.createSender("test") set_phone(app) response = client_nc.post( "/us-send-code", json=dict(identity="*****@*****.**", chosen_method="sms"), headers=headers, follow_redirects=True, ) assert response.status_code == 200 code = sms_sender.messages[0].split()[-1].strip(".") response = client_nc.post( "/us-signin?include_auth_token", json=dict(identity="*****@*****.**", passcode=code), headers=headers, follow_redirects=True, ) assert response.status_code == 200 assert "authentication_token" in response.json["response"]["user"] assert len(flashes) == 0