def test_default_unauthz(app, client): """ Test default unauthz handler with and without json """ authenticate(client, "*****@*****.**", "password") response = client.get("/admin") # This is the result of abort(403) since there is no UNAUTHORIZED_VIEW assert response.status_code == 403 response = client.get("/admin", headers={"Accept": "application/json"}) assert response.status_code == 403 assert response.json["meta"]["code"] == 403
def test_default_unauthz_myjson(app, client): """ Make sure render_json gets called for unauthn errors """ @app.security.render_json def my_json(payload, code, headers=None, user=None): return jsonify(dict(myresponse=payload, code=code)), code, headers authenticate(client, "*****@*****.**", "password") response = client.get("/admin", headers={"Accept": "application/json"}) assert response.status_code == 403 assert response.json["code"] == 403
def test_authn_freshness_callable(app, client, get_message): @auth_required(within=lambda: timedelta(minutes=30)) def myview(): return Response(status=200) app.add_url_rule("/myview", view_func=myview, methods=["GET"]) authenticate(client) # This should work and not be redirected response = client.get("/myview", follow_redirects=False) assert response.status_code == 200
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_authr_identity(app, client): # Setup authenticator headers = {"Accept": "application/json", "Content-Type": "application/json"} authenticate(client, email="*****@*****.**") setup_data = dict(setup="authenticator") response = client.post("/tf-setup", json=setup_data, headers=headers) assert response.json["response"]["tf_authr_issuer"] == "service_name" assert response.json["response"]["tf_authr_username"] == "jill" assert response.json["response"]["tf_state"] == "validating_profile" assert "tf_authr_key" in response.json["response"]
def test_xlation(app, client): app.config["BABEL_DEFAULT_LOCALE"] = "fr_FR" assert check_xlation( app, "fr_FR"), "You must run python setup.py compile_catalog" response = client.get("/login") assert b'<label for="password">Mot de passe</label>' in response.data response = authenticate(client) assert response.status_code == 302 response = authenticate(client, follow_redirects=True) assert b"Bienvenue [email protected]" in response.data
def test_email_salutation(app, client): authenticate(client, email="*****@*****.**") with app.mail.record_messages() as outbox: response = client.post( "/tf-setup", data=dict(setup="email"), follow_redirects=True ) msg = b"To complete logging in, please enter the code sent to your mail" assert msg in response.data assert "*****@*****.**" in outbox[0].send_to assert "*****@*****.**" in outbox[0].body assert "*****@*****.**" in outbox[0].html
def test_override_length(app, client, get_message): authenticate(client) response = client.post( "/change", data={ "password": "******", "new_password": "******", "new_password_confirm": "01234567890", }, follow_redirects=True, ) assert get_message("PASSWORD_INVALID_LENGTH", length=12) in response.data
def test_authenticated_loop(client): # If user is already authenticated say via session, and then hits an endpoint # protected with @auth_token_required() - then they will be redirected to the login # page which will simply note the current user is already logged in and redirect # to POST_LOGIN_VIEW. Between 3.3.0 and 3.4.4 - this redirect would honor the 'next' # parameter - thus redirecting back to the endpoint that caused the redirect in the # first place - thus an infinite loop. authenticate(client) response = client.get("/token", follow_redirects=True) assert response.status_code == 200 assert b"Home Page" 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_basic_custom_forms(app, sqlalchemy_datastore): class MyLoginForm(LoginForm): email = StringField("My Login Email Address Field") class MyRegisterForm(RegisterForm): email = StringField("My Register Email Address Field") class MyForgotPasswordForm(ForgotPasswordForm): email = StringField( "My Forgot Email Address Field", validators=[email_required, email_validator, valid_user_email], ) class MyResetPasswordForm(ResetPasswordForm): password = StringField("My Reset Password Field") class MyChangePasswordForm(ChangePasswordForm): password = PasswordField("My Change Password Field") app.security = Security( app, datastore=sqlalchemy_datastore, login_form=MyLoginForm, register_form=MyRegisterForm, forgot_password_form=MyForgotPasswordForm, reset_password_form=MyResetPasswordForm, change_password_form=MyChangePasswordForm, ) populate_data(app) client = app.test_client() response = client.get("/login") assert b"My Login Email Address Field" in response.data response = client.get("/register") assert b"My Register Email Address Field" in response.data response = client.get("/reset") assert b"My Forgot Email Address Field" in response.data with capture_reset_password_requests() as requests: response = client.post("/reset", data=dict(email="*****@*****.**")) token = requests[0]["token"] response = client.get("/reset/" + token) assert b"My Reset Password Field" in response.data authenticate(client) response = client.get("/change") assert b"My Change Password Field" in response.data
def test_custom_post_change_view(client): authenticate(client) response = client.post( "/change", data={ "password": "******", "new_password": "******", "new_password_confirm": "new strong password", }, follow_redirects=True, ) assert b"Profile Page" in response.data
def test_authn_via(app, client, get_message): """ Test that we get correct fs_authn_via set in request """ @auth_required(within=30, grace=0) def myview(): assert get_request_attr("fs_authn_via") == "session" return Response(status=200) app.add_url_rule("/myview", view_func=myview, methods=["GET"]) authenticate(client) # This should work and not be redirected response = client.get("/myview", follow_redirects=False) assert response.status_code == 200
def test_just_authenticator(app, client): authenticate(client, email="*****@*****.**") response = client.get("/tf-setup", follow_redirects=True) assert b"Set up using SMS" not in response.data data = dict(setup="authenticator") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"Submit Code" in response.data # test json response = client.post("/tf-setup", json=data) assert response.status_code == 200
def test_my_unauthz_handler_exc(app, client): """ Verify that can use exceptions in unauthz handler """ @app.security.unauthz_handler def my_unauthz(func, params): raise ValueError("Bad Value") @app.errorhandler(ValueError) def error_handler(ex): return jsonify(dict(code=403)), 403 authenticate(client, "*****@*****.**", "password") response = client.get("/admin", headers={"Accept": "application/json"}) assert response.status_code == 403
def test_easy_password(app, client): authenticate(client) data = ('{"password": "******", ' '"new_password": "******", ' '"new_password_confirm": "mattmatt2"}') response = client.post("/change", data=data, headers={"Content-Type": "application/json"}) assert response.headers["Content-Type"] == "application/json" assert response.status_code == 400 # Response from zxcvbn assert "Repeats like" in response.json["response"]["errors"][ "new_password"][0]
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_set_unauthorized_handler(app, client): @app.security.unauthorized_handler def unauthorized(): app.unauthorized_handler_set = True return "unauthorized-handler-set", 401 app.unauthorized_handler_set = False authenticate(client, "*****@*****.**") response = client.get("/admin", follow_redirects=True) assert app.unauthorized_handler_set is True assert b"unauthorized-handler-set" in response.data assert response.status_code == 401
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_logout_methods_none(app, sqlalchemy_datastore): init_app_with_options(app, sqlalchemy_datastore, **{"SECURITY_LOGOUT_METHODS": None}) client = app.test_client() authenticate(client) response = client.get("/logout", follow_redirects=True) assert response.status_code == 404 response = client.post("/logout", follow_redirects=True) assert response.status_code == 404
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_verify_next(app, client, get_message): authenticate(client) response = client.post( "/auth/?next=http://localhost/mynext", data=dict(password="******"), follow_redirects=False, ) assert response.location == "http://localhost/mynext" response = client.post( "/auth/?next=http%3A%2F%2F127.0.0.1%3A5000%2Fdashboard%2Fsettings%2F", data=dict(password="******"), follow_redirects=False, base_url="http://127.0.0.1:5000", ) assert response.location == "http://127.0.0.1:5000/dashboard/settings/"
def test_requires_confirmation_error_redirect(app, clients): data = dict(email="*****@*****.**", password="******") response = clients.post("/register", data=data) response = authenticate(clients, **data, follow_redirects=True) assert b"send_confirmation_form" in response.data assert b"*****@*****.**" in response.data
def test_unicode_invalid_length(app, client, get_message): # From NIST and OWASP - each unicode code point should count as a character. authenticate(client) # Emoji's are 4 bytes in utf-8 data = dict( password="******", new_password="******", new_password_confirm="\N{CYCLONE}\N{CYCLONE}\N{FOGGY}\N{FOGGY}", ) response = client.post("/change", json=data, headers={"Content-Type": "application/json"}) assert response.headers["Content-Type"] == "application/json" assert response.status_code == 400 assert get_message("PASSWORD_INVALID_LENGTH", length=8) in response.data
def test_my_unauthz_handler(app, client): @app.security.unauthz_handler def my_unauthz(func, params): return ( jsonify( dict(myresponse={"func": func.__name__, "params": params}, code=403) ), 403, ) authenticate(client, "*****@*****.**", "password") response = client.get("/admin", headers={"Accept": "application/json"}) assert response.status_code == 403 assert response.json["code"] == 403 assert response.json["myresponse"]["func"] == "roles_required" assert response.json["myresponse"]["params"] == ["admin"]
def test_default_authn_bp(app, client): """Test default reauthn handler with blueprint prefix""" @auth_required(within=1, grace=0) def myview(): return Response(status=200) app.add_url_rule("/myview", view_func=myview, methods=["GET"]) authenticate(client, endpoint="/myprefix/login") # This should require additional authn and redirect to verify reset_fresh(client, within=timedelta(minutes=1)) response = client.get("/myview", follow_redirects=False) assert response.status_code == 302 assert ( response.location == "http://localhost/myprefix/verify?next=http%3A%2F%2Flocalhost%2Fmyview" )
def test_post_already_authenticated(client): response = authenticate(client, follow_redirects=True) assert b"Welcome [email protected]" in response.data data = dict(email="*****@*****.**", password="******") response = client.post("/login", data=data, follow_redirects=True) assert b"Post Login" in response.data # This should NOT override post_login_view due to potential redirect loops. response = client.post("/login?next=/page1", data=data, follow_redirects=True) assert b"Post Login" in response.data
def test_qrcode_identity(app, client): # Setup authenticator authenticate(client, email="*****@*****.**") 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. Verify that we get 'username' not email. 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 == "jill" assert b"svg" in qrcode_page_response.data
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_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