def test_used_reset_token(client, get_message): with capture_reset_password_requests() as requests: client.post("/reset", data=dict(email="*****@*****.**"), follow_redirects=True) token = requests[0]["token"] # use the token response = client.post( "/reset/" + token, data={ "password": "******", "password_confirm": "awesome sunset" }, follow_redirects=True, ) assert get_message("PASSWORD_RESET") in response.data logout(client) # attempt to use it a second time response2 = client.post( "/reset/" + token, data={ "password": "******", "password_confirm": "otherpassword" }, follow_redirects=True, ) msg = get_message("INVALID_RESET_PASSWORD_TOKEN") assert msg in response2.data
def test_tf_reset_invalidates_cookie(app, client): tf_authenticate(app, client, remember=True) logout(client) 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", data=data, follow_redirects=True) assert b"Two-factor authentication adds an extra layer of security" in response.data client.cookie_jar.clear("localhost.local", "/", "tf_validity") # Test JSON token = tf_authenticate(app, client, json=True, remember=True, validate=False) logout(client) 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="******", tf_validity_token=token) response = client.post( "/login", json=data, follow_redirects=True, headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 assert response.json["response"]["tf_required"] assert response.json["response"]["tf_state"] == "setup_from_login"
def test_nullable_username(app, sqlalchemy_datastore): # sqlalchemy datastore uses fsqlav2 which has username as unique and nullable # make sure can register multiple users with no username # Note that current WTForms (2.2.1) has a bug where StringFields can never be # None - it changes them to an empty string. DBs don't like that if you have # your column be 'nullable'. class NullableStringField(StringField): def process_formdata(self, valuelist): if valuelist: self.data = valuelist[0] class MyRegisterForm(ConfirmRegisterForm): username = NullableStringField("Username") app.config["SECURITY_CONFIRM_REGISTER_FORM"] = MyRegisterForm security = Security(datastore=sqlalchemy_datastore) security.init_app(app) client = app.test_client() data = dict(email="*****@*****.**", password="******", password_confirm="password") response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 200 logout(client) data = dict(email="*****@*****.**", password="******", password_confirm="password") response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 200
def test_trackable_using_login_user(app, client): """ This tests is only to serve as an example of how one needs to call datastore.commit() after logging a user in to make sure the trackable fields are saved to the datastore. """ app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1) @app.route("/login_custom", methods=["POST"]) def login_custom(): user = app.security.datastore.find_user(email=e) login_user(user) @after_this_request def save_user(response): app.security.datastore.commit() return response return redirect("/") e = "*****@*****.**" authenticate(client, email=e) logout(client) data = dict(email=e, password="******", remember="y") client.post("/login_custom", data=data, headers={"X-Forwarded-For": "127.0.0.1"}) with app.app_context(): user = app.security.datastore.find_user(email=e) assert user.last_login_at is not None assert user.current_login_at is not None assert user.last_login_ip == _client_ip(client) assert user.current_login_ip == "127.0.0.1" assert user.login_count == 2
def test_do_not_remember_tf_validity(app, client): tf_authenticate(app, client) logout(client) data = dict(email="*****@*****.**", password="******") response = client.post("/login", data=data, follow_redirects=True) assert b"Please enter your authentication code" in response.data # Test JSON token = tf_authenticate(app, client, json=True) logout(client) assert token is None data = dict(email="*****@*****.**", password="******", tf_validity_token=token) response = client.post( "/login", json=data, follow_redirects=True, 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"
def test_change_uniquifier_invalidates_cookie(app, client): tf_authenticate(app, client, remember=True) logout(client) with app.app_context(): user = app.security.datastore.find_user(email="*****@*****.**") app.security.datastore.set_uniquifier(user) app.security.datastore.commit() data = dict(email="*****@*****.**", password="******") response = client.post("/login", data=data, follow_redirects=True) assert b"Please enter your authentication code" in response.data client.cookie_jar.clear("localhost.local", "/", "tf_validity") # Test JSON token = tf_authenticate(app, client, json=True, remember=True) logout(client) with app.app_context(): user = app.security.datastore.find_user(email="*****@*****.**") app.security.datastore.set_uniquifier(user) app.security.datastore.commit() data = dict(email="*****@*****.**", password="******", tf_validity_token=token) response = client.post( "/login", json=data, follow_redirects=True, 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"
def test_two_factor_context_processors(client, app): # Test two factor context processors @app.security.context_processor def default_ctx_processor(): return {"global": "global"} @app.security.tf_setup_context_processor def send_two_factor_setup(): return {"foo": "bar-tfsetup"} # Note this just does initial login on a user that hasn't setup 2FA yet. authenticate(client) response = client.get("/tf-setup") assert b"global" in response.data assert b"bar-tfsetup" in response.data logout(client) @app.security.tf_token_validation_context_processor def send_two_factor_token_validation(): return {"foo": "bar-tfvalidate"} tf_authenticate(app, client, validate=False) response = client.get("/tf-rescue") assert b"global" in response.data assert b"bar-tfvalidate" in response.data logout(client)
def test_tf_select_json(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) setup_tf(client) logout(client) # since we have 2 2FA methods configured - we should get the tf-select form response = client.post("/login", json=dict(email="*****@*****.**", password="******")) assert response.json["response"]["tf_required"] choices = response.json["response"]["tf_setup_methods"] assert all(k in choices for k in ["sms", "webauthn"]) # use webauthn as the second factor response = client.post("/tf-select", json=dict(which="webauthn")) signin_url = response.json["response"]["tf_signin_url"] headers = { "Accept": "application/json", "Content-Type": "application/json" } response = client.post(signin_url, headers=headers) response_url = f'wan-signin/{response.json["response"]["wan_state"]}' response = client.post( response_url, json=dict(credential=json.dumps(wankeys["secondary"]["signin"])), ) assert response.status_code == 200 assert not tf_in_session(get_existing_session(client)) response = client.get("/profile", follow_redirects=False) assert response.status_code == 200
def test_confirmation_different_user_when_logged_in_no_auto( client, get_message): """ Default - AUTO_LOGIN == false so shouldn't log in second user. """ e1 = "*****@*****.**" e2 = "*****@*****.**" with capture_registrations() as registrations: for e in e1, e2: data = dict(email=e, password="******", next="") client.post("/register", data=data) logout(client) token1 = registrations[0]["confirm_token"] token2 = registrations[1]["confirm_token"] client.get("/confirm/" + token1, follow_redirects=True) logout(client) authenticate(client, email=e1) response = client.get("/confirm/" + token2, follow_redirects=True) assert get_message("EMAIL_CONFIRMED") in response.data # should get a login view assert ( b'<input id="password" name="password" required type="password" value="">' in response.data)
def test_password_normalization(app, client, get_message): with capture_reset_password_requests() as requests: response = client.post( "/reset", json=dict(email="*****@*****.**"), ) assert response.status_code == 200 token = requests[0]["token"] response = client.post( "/reset/" + token, json=dict(password="******", password_confirm="HöheHöhe"), ) assert response.status_code == 200 logout(client) # make sure can log in with new password both normnalized or not response = client.post( "/login", json=dict(email="*****@*****.**", password="******"), ) assert response.status_code == 200 # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 logout(client) response = client.post( "/login", json=dict(email="*****@*****.**", password="******"), ) assert response.status_code == 200 # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200
def test_cp_config2(app, client): # Test improper config (must have CSRFProtect configured if setting # CSRF_PROTECT_MECHANISMS app.config["WTF_CSRF_ENABLED"] = True # The check is done on first request. with pytest.raises(ValueError) as ev: logout(client) assert "CsrfProtect not part of application" in str(ev.value)
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_permissions_required(clients): for user in ["*****@*****.**"]: authenticate(clients, user) response = clients.get("/admin_perm_required") assert b"Admin Page required" in response.data logout(clients) authenticate(clients, "*****@*****.**") response = clients.get("/admin_perm_required", follow_redirects=True) assert b"Unauthorized" in response.data
def test_permissions_accepted(clients): for user in ("*****@*****.**", "*****@*****.**"): authenticate(clients, user) response = clients.get("/admin_perm") assert b"Admin Page with full-write or super" in response.data logout(clients) authenticate(clients, "*****@*****.**") response = clients.get("/admin_perm", follow_redirects=True) assert b"Unauthorized" in response.data
def test_cp_config(app, client): # Test improper config (must have WTF_CSRF_CHECK_DEFAULT false if setting # CSRF_PROTECT_MECHANISMS app.config["WTF_CSRF_ENABLED"] = True CSRFProtect(app) # The check is done on first request. with pytest.raises(ValueError) as ev: logout(client) assert "must be set to False" in str(ev.value)
def test_login_csrf_unauth_ok(app, client): app.config["WTF_CSRF_ENABLED"] = True with mp_validate_csrf() as mp: # This should log in. data = dict(email="*****@*****.**", password="******", remember="y") response = client.post("/login", data=data, follow_redirects=True) assert response.status_code == 200 assert b"Welcome matt" in response.data assert mp.success == 0 and mp.failure == 0 logout(client)
def test_roles_accepted(clients): # This specificaly tests that we can pass a URL for unauthorized_view. for user in ("*****@*****.**", "*****@*****.**"): authenticate(clients, user) response = clients.get("/admin_or_editor") assert b"Admin or Editor Page" in response.data logout(clients) authenticate(clients, "*****@*****.**") response = clients.get("/admin_or_editor", follow_redirects=True) assert b"Unauthorized" in response.data
def test_pwd_no_normalize(app, client): """Verify that can log in with original but not normalized if have disabled normalization """ authenticate(client) data = dict( password="******", new_password="******", new_password_confirm="new strong password\N{ROMAN NUMERAL ONE}", ) response = client.post( "/change", json=data, headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 logout(client) # try with normalized password - should fail response = client.post( "/login", json=dict( email="*****@*****.**", password="******", ), headers={"Content-Type": "application/json"}, ) assert response.status_code == 400 # use original typed-in pwd response = client.post( "/login", json=dict( email="*****@*****.**", password="******" ), headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 # Verify can change password using original password data = dict( password="******", new_password="******", new_password_confirm="new strong password\N{ROMAN NUMERAL TWO}", ) response = client.post( "/change", json=data, headers={"Content-Type": "application/json"}, ) assert response.status_code == 200
def test_email_not_identity(app, sqlalchemy_datastore, get_message): # Test that can register/confirm with email even if it isn't an IDENTITY_ATTRIBUTE from flask_security import ConfirmRegisterForm, Security, unique_identity_attribute from wtforms import StringField, validators class MyRegisterForm(ConfirmRegisterForm): username = StringField( "Username", validators=[validators.data_required(), unique_identity_attribute], ) app.config["SECURITY_CONFIRM_REGISTER_FORM"] = MyRegisterForm security = Security(datastore=sqlalchemy_datastore) security.init_app(app) client = app.test_client() with capture_registrations() as registrations: data = dict(email="*****@*****.**", username="******", password="******") response = client.post("/register", data=data, follow_redirects=True) assert b"*****@*****.**" in response.data token = registrations[0]["confirm_token"] response = client.get("/confirm/" + token, headers={"Accept": "application/json"}) assert response.status_code == 302 assert response.location == "http://localhost/" logout(client) # check that username must be unique data = dict(email="*****@*****.**", username="******", password="******") response = client.post("/register", data=data, headers={"Accept": "application/json"}) assert response.status_code == 400 assert "is already associated" in response.json["response"]["errors"][ "username"][0] # log in with username - this uses the age-old hack that although the form's # input label says "email" - it in fact will accept any identity attribute. response = client.post( "/login", data=dict(email="mary", password="******"), follow_redirects=True, ) assert b"<p>Welcome mary</p>" in response.data
def test_trackable_flag(app, client): app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1) e = "*****@*****.**" authenticate(client, email=e) logout(client) authenticate(client, email=e, headers={"X-Forwarded-For": "127.0.0.1"}) with app.app_context(): user = app.security.datastore.find_user(email=e) assert user.last_login_at is not None assert user.current_login_at is not None assert user.last_login_ip == _client_ip(client) assert user.current_login_ip == "127.0.0.1" assert user.login_count == 2
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_allow_null_password_nologin(client, get_message): # If unified sign in is enabled - should be able to register w/o password # With confirmable false - should be logged in automatically upon register. # But shouldn't be able to perform normal login again data = dict(email="*****@*****.**", password="") response = client.post("/register", data=data, follow_redirects=True) assert b"Welcome [email protected]" in response.data logout(client) # Make sure can't log in response = authenticate(client, email="*****@*****.**", password="") assert get_message("PASSWORD_NOT_PROVIDED") in response.data response = authenticate(client, email="*****@*****.**", password="******") assert get_message("INVALID_PASSWORD") in response.data
def test_rescue_json(app, client): headers = { "Accept": "application/json", "Content-Type": "application/json" } # it's an error if not logged in. rescue_data_json = dict(help_setup="lost_device") response = client.post( "/tf-rescue", json=rescue_data_json, headers=headers, ) assert response.status_code == 400 # check when two_factor_rescue function should appear data = dict(email="*****@*****.**", password="******") response = client.post("/login", json=data, headers=headers) assert response.json["response"]["tf_required"] with app.mail.record_messages() as outbox: rescue_data = dict(help_setup="lost_device") response = client.post("/tf-rescue", json=rescue_data, headers=headers) assert response.status_code == 200 assert outbox[0].recipients == ["*****@*****.**"] assert outbox[0].sender == "no-reply@localhost" assert outbox[0].subject == "Two-factor Login" matcher = re.match(r".*code: ([0-9]+).*", outbox[0].body, re.IGNORECASE | re.DOTALL) response = client.post("/tf-validate", json=dict(code=matcher.group(1)), headers=headers) assert response.status_code == 200 logout(client) # Try rescue with no email (should send email to admin) client.post("/login", json=data, headers=headers) with app.mail.record_messages() as outbox: rescue_data = dict(help_setup="no_mail_access") response = client.post("/tf-rescue", json=rescue_data, headers=headers) assert response.status_code == 200 assert outbox[0].recipients == ["*****@*****.**"] assert outbox[0].sender == "no-reply@localhost" assert outbox[0].subject == "Two-factor Rescue" assert "*****@*****.**" in outbox[0].body
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_nullable_username(app, client): # sqlalchemy datastore uses fsqlav2 which has username as unique and nullable # make sure can register multiple users with no username # Note that current WTForms (2.2.1) has a bug where StringFields can never be # None - it changes them to an empty string. DBs don't like that if you have # your column be 'nullable'. data = dict(email="*****@*****.**", password="******") response = client.post("/register", json=data, headers={"Content-Type": "application/json"}) assert response.status_code == 200 logout(client) data = dict(email="*****@*****.**", password="******") response = client.post("/register", json=data, headers={"Content-Type": "application/json"}) assert response.status_code == 200
def test_trackable_with_multiple_ips_in_headers(app, client): app.wsgi_app = ProxyFix(app.wsgi_app, x_for=2) e = "*****@*****.**" authenticate(client, email=e) logout(client) authenticate( client, email=e, headers={"X-Forwarded-For": "99.99.99.99, 88.88.88.88, 77.77.77.77"}, ) with app.app_context(): user = app.security.datastore.find_user(email=e) assert user.last_login_at is not None assert user.current_login_at is not None assert user.last_login_ip == _client_ip(client) assert user.current_login_ip == "88.88.88.88" assert user.login_count == 2
def test_username_normalize(app, client, get_message): data = dict( email="*****@*****.**", username="******", password="******", ) response = client.post("/register", json=data, headers={"Content-Type": "application/json"}) assert response.status_code == 200 logout(client) response = client.post( "/us-signin", data=dict(identity="Imnumber\N{LATIN CAPITAL LETTER I}", passcode="awesome sunset"), follow_redirects=True, ) assert b"Welcome [email protected]" in response.data
def test_verify(app, client, get_message): # Test setup when re-authenticate required authenticate(client) response = client.get("tf-setup", follow_redirects=False) verify_url = response.location assert ( verify_url == "http://localhost/verify?next=http%3A%2F%2Flocalhost%2Ftf-setup" ) logout(client) # Now try again - follow redirects to get to verify form # This call should require re-verify authenticate(client) response = client.get("tf-setup", follow_redirects=True) form_response = response.data.decode("utf-8") assert get_message("REAUTHENTICATION_REQUIRED") in response.data matcher = re.match( r'.*form action="([^"]*)".*', form_response, re.IGNORECASE | re.DOTALL ) verify_password_url = matcher.group(1) # Send wrong password response = client.post( verify_password_url, data=dict(password="******"), follow_redirects=True, ) assert response.status_code == 200 assert get_message("INVALID_PASSWORD") in response.data # Verify with correct password with capture_flashes() as flashes: response = client.post( verify_password_url, data=dict(password="******"), follow_redirects=False, ) assert response.status_code == 302 assert response.location == "http://localhost/tf-setup" assert get_message("REAUTHENTICATION_SUCCESSFUL") == flashes[0]["message"].encode( "utf-8" )
def test_confirmation_different_user_when_logged_in(client, get_message): e1 = "*****@*****.**" e2 = "*****@*****.**" with capture_registrations() as registrations: for e in e1, e2: data = dict(email=e, password="******", next="") client.post("/register", data=data) logout(client) token1 = registrations[0]["confirm_token"] token2 = registrations[1]["confirm_token"] client.get("/confirm/" + token1, follow_redirects=True) logout(client) authenticate(client, email=e1) response = client.get("/confirm/" + token2, follow_redirects=True) assert get_message("EMAIL_CONFIRMED") in response.data assert b"Welcome [email protected]" in response.data
def test_direct_decorator(app, client, get_message): """ Test/show calling the auth_required decorator directly """ headers = {"Accept": "application/json", "Content-Type": "application/json"} def myview(): return roles_required("author")(domyview)() def domyview(): return Response(status=200) app.add_url_rule("/myview", view_func=myview, methods=["GET"]) authenticate(client) response = client.get("/myview", headers=headers) assert response.status_code == 403 logout(client) authenticate(client, email="*****@*****.**") response = client.get("/myview", headers=headers) assert response.status_code == 200