From b759a146344d86d2bc477115ff459299965076d0 Mon Sep 17 00:00:00 2001 From: Chris Wagner Date: Wed, 1 Jan 2025 14:07:25 -0800 Subject: [PATCH] Chore for 5.6 - change version strings to 5.6 - remove deprecated TWO_FACTOR config variables - tighten up init check for bleach - only if they are using our username_util - Add some 2025 copyrights! - document LoginForm --- CHANGES.rst | 10 ++++++-- LICENSE.txt | 2 +- docs/conf.py | 2 +- docs/configuration.rst | 17 ------------- flask_security/__init__.py | 4 +-- flask_security/core.py | 23 +++-------------- flask_security/forms.py | 43 +++++++++++++++++++++----------- flask_security/recovery_codes.py | 4 +-- flask_security/unified_signin.py | 6 ++--- flask_security/webauthn.py | 6 ++--- tests/conftest.py | 3 ++- tests/test_misc.py | 8 +++--- tests/test_two_factor.py | 2 +- tests/test_unified_signin.py | 2 +- tox.ini | 2 +- 15 files changed, 61 insertions(+), 73 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 26f31191..60d42863 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,10 +10,11 @@ Released TBD Features & Improvements +++++++++++++++++++++++ -- (:issue:`1038`) Add support for 'secret_key' rotation -- (:issue:`980`) Add support for username recovery in simple login flows +- (:issue:`1038`) Add support for 'secret_key' rotation (jamesejr) +- (:issue:`980`) Add support for username recovery in simple login flows (jamesejr) - (:pr:`1048`) Add support for Python 3.13 - (:issue:`1043`) Unify Register forms (and split out re-type password option) +- (:pr:`xx`) Remove deprecated TWO_FACTOR configuration variables Notes +++++ @@ -29,6 +30,11 @@ The register forms have been combined - or more accurately - there is a new Regi that subsumes the features of both the old RegisterForm and ConfirmRegisterForm. Please read :ref:`register_form_migration`. +The SECURITY_TWO_FACTOR_{SECRET, URI_SERVICE_NAME, SMS_SERVICE, SMS_SERVICE_CONFIG} +have been removed (they have been deprecated for a while). Use the equivalent +:py:data:`SECURITY_TOTP_SECRETS`, :py:data:`SECURITY_TOTP_ISSUER`, :py:data:`SECURITY_SMS_SERVICE` and +:py:data:`SECURITY_SMS_SERVICE_CONFIG`. + Version 5.5.2 ------------- diff --git a/LICENSE.txt b/LICENSE.txt index d165be9a..6de53fe3 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,7 +1,7 @@ MIT License Copyright (C) 2012-2021 by Matthew Wright -Copyright (C) 2019-2024 by Chris Wagner +Copyright (C) 2019-2025 by Chris Wagner Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/docs/conf.py b/docs/conf.py index ba867150..945ad42f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -51,7 +51,7 @@ # General information about the project. project = "Flask-Security" -copyright = "2012-2024" +copyright = "2012-2025" author = "Matt Wright & Chris Wagner" # The version info for the project you're documenting, acts as replacement for diff --git a/docs/configuration.rst b/docs/configuration.rst index 734332a4..e6874673 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -1261,23 +1261,6 @@ Configuration related to the two-factor authentication feature. Specifies the default enabled methods for two-factor authentication. Default: ``['email', 'authenticator', 'sms']`` which are the only currently supported methods. - -.. py:data:: SECURITY_TWO_FACTOR_SECRET - - .. deprecated:: 3.4.0 see: :py:data:`SECURITY_TOTP_SECRETS` - -.. py:data:: SECURITY_TWO_FACTOR_URI_SERVICE_NAME - - .. deprecated:: 3.4.0 see: :py:data:`SECURITY_TOTP_ISSUER` - -.. py:data:: SECURITY_TWO_FACTOR_SMS_SERVICE - - .. deprecated:: 3.4.0 see: :py:data:`SECURITY_SMS_SERVICE` - -.. py:data:: SECURITY_TWO_FACTOR_SMS_SERVICE_CONFIG - - .. deprecated:: 3.4.0 see: :py:data:`SECURITY_SMS_SERVICE_CONFIG` - .. py:data:: SECURITY_TWO_FACTOR_AUTHENTICATOR_VALIDITY Specifies the number of seconds access token is valid. diff --git a/flask_security/__init__.py b/flask_security/__init__.py index 1c071191..66e6bfea 100644 --- a/flask_security/__init__.py +++ b/flask_security/__init__.py @@ -6,7 +6,7 @@ to Flask applications. :copyright: (c) 2012-2019 by Matt Wright. - :copyright: (c) 2019-2024 by J. Christopher Wagner. + :copyright: (c) 2019-2025 by J. Christopher Wagner. :license: MIT, see LICENSE for more details. """ @@ -142,4 +142,4 @@ ) from .webauthn_util import WebauthnUtil -__version__ = "5.5.2" +__version__ = "5.6.0" diff --git a/flask_security/core.py b/flask_security/core.py index 36f52d69..5739b4f6 100644 --- a/flask_security/core.py +++ b/flask_security/core.py @@ -7,7 +7,7 @@ :copyright: (c) 2012 by Matt Wright. :copyright: (c) 2017 by CERN. :copyright: (c) 2017 by ETH Zurich, Swiss Data Science Center. - :copyright: (c) 2019-2024 by J. Christopher Wagner (jwag). + :copyright: (c) 2019-2025 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ @@ -317,15 +317,7 @@ "PHONE_NUMBER": None, }, "TWO_FACTOR_REQUIRED": False, - "TWO_FACTOR_SECRET": None, # Deprecated - use TOTP_SECRETS "TWO_FACTOR_ENABLED_METHODS": ["email", "authenticator", "sms"], - "TWO_FACTOR_URI_SERVICE_NAME": "service_name", # Deprecated - use TOTP_ISSUER - "TWO_FACTOR_SMS_SERVICE": "Dummy", # Deprecated - use SMS_SERVICE - "TWO_FACTOR_SMS_SERVICE_CONFIG": { # Deprecated - use SMS_SERVICE_CONFIG - "ACCOUNT_SID": None, - "AUTH_TOKEN": None, - "PHONE_NUMBER": None, - }, "TWO_FACTOR_IMPLEMENTATIONS": { "code": "flask_security.twofactor.CodeTfPlugin", "webauthn": "flask_security.webauthn.WebAuthnTfPlugin", @@ -1548,6 +1540,7 @@ def init_app( "recoverable", "two_factor", "unified_signin", + "username_recovery", "passwordless", "webauthn", "mail_util_cls", @@ -1702,16 +1695,6 @@ def init_app( if rn := cv("CLI_ROLES_NAME", app, strict=True): app.cli.add_command(roles, rn) - # Migrate from TWO_FACTOR config to generic config. - for newc, oldc in [ - ("SECURITY_SMS_SERVICE", "SECURITY_TWO_FACTOR_SMS_SERVICE"), - ("SECURITY_SMS_SERVICE_CONFIG", "SECURITY_TWO_FACTOR_SMS_SERVICE_CONFIG"), - ("SECURITY_TOTP_SECRETS", "SECURITY_TWO_FACTOR_SECRET"), - ("SECURITY_TOTP_ISSUER", "SECURITY_TWO_FACTOR_URI_SERVICE_NAME"), - ]: - if not app.config.get(newc, None): - app.config[newc] = app.config.get(oldc, None) - # Alternate/code authentication configuration checks and setup alt_auth = False if cv("UNIFIED_SIGNIN", app=app): @@ -1775,7 +1758,7 @@ def init_app( if cv("WEBAUTHN", app=app): self._check_modules("webauthn", "WEBAUTHN") - if cv("USERNAME_ENABLE", app=app): + if cv("USERNAME_ENABLE", app=app) and self.username_util_cls == UsernameUtil: self._check_modules("bleach", "USERNAME_ENABLE") # Register so other packages can reference our translations. diff --git a/flask_security/forms.py b/flask_security/forms.py index a9f11117..24d36946 100644 --- a/flask_security/forms.py +++ b/flask_security/forms.py @@ -30,11 +30,10 @@ SubmitField, TelField, ValidationError, - validators, ) from werkzeug.datastructures import MultiDict -from wtforms.validators import Optional, StopValidation +from wtforms.validators import Optional, StopValidation, EqualTo, DataRequired, Length from .babel import is_lazy_string, make_lazy_string from .confirmable import requires_confirmation @@ -135,15 +134,15 @@ def __call__(self, form, field): return super().__call__(form, field) -class EqualTo(ValidatorMixin, validators.EqualTo): +class EqualToLocalize(ValidatorMixin, EqualTo): pass -class Required(ValidatorMixin, validators.DataRequired): +class RequiredLocalize(ValidatorMixin, DataRequired): pass -class Length(ValidatorMixin, validators.Length): +class LengthLocalize(ValidatorMixin, Length): pass @@ -181,8 +180,8 @@ def __call__(self, form, field): raise StopValidation(msg) -email_required = Required(message="EMAIL_NOT_PROVIDED") -password_required = Required(message="PASSWORD_NOT_PROVIDED") +email_required = RequiredLocalize(message="EMAIL_NOT_PROVIDED") +password_required = RequiredLocalize(message="PASSWORD_NOT_PROVIDED") def _local_xlate(text): @@ -349,7 +348,7 @@ class PasswordConfirmFormMixin: get_form_field_label("retype_password"), render_kw={"autocomplete": "new-password"}, validators=[ - EqualTo("password", message="RETYPE_PASSWORD_MISMATCH"), + EqualToLocalize("password", message="RETYPE_PASSWORD_MISMATCH"), password_required, ], ) @@ -364,7 +363,9 @@ def build_password_field(is_confirm=False, autocomplete="new-password", app=None validators.append(password_required) if is_confirm: - validators.append(EqualTo("password", message="RETYPE_PASSWORD_MISMATCH")) + validators.append( + EqualToLocalize("password", message="RETYPE_PASSWORD_MISMATCH") + ) return PasswordField( label=get_form_field_label("retype_password"), render_kw=render_kw, @@ -397,14 +398,14 @@ class CodeFormMixin: "inputtype": "numeric", "pattern": "[0-9]*", }, - validators=[Required()], + validators=[RequiredLocalize()], ) def build_register_username_field(app): if cv("USERNAME_REQUIRED", app=app): validators = [ - Required(message="USERNAME_NOT_PROVIDED"), + RequiredLocalize(message="USERNAME_NOT_PROVIDED"), username_validator, unique_username, ] @@ -525,7 +526,19 @@ def validate(self, **kwargs: t.Any) -> bool: class LoginForm(Form, PasswordFormMixin, NextFormMixin): - """The default login form""" + """The default login form + + The following fields are defined: + * email + * username (based on :py:data:`SECURITY_USERNAME_ENABLE`) + * password + * remember (checkbox) + * next + + If a subclass wants to handle identity, it can set self.ifield to the + form field that it validated. That will cause the validation logic here around + identity to be skipped. The subclass must also set self.user to the found User. + """ # email field - we don't use valid_user_email since for login # with username feature it is potentially optional. @@ -703,8 +716,8 @@ class RegisterForm(ConfirmRegisterForm, NextFormMixin): password_confirm = PasswordField( get_form_field_label("retype_password"), validators=[ - EqualTo("password", message="RETYPE_PASSWORD_MISMATCH"), - validators.Optional(), + EqualToLocalize("password", message="RETYPE_PASSWORD_MISMATCH"), + Optional(), ], ) @@ -859,7 +872,7 @@ class ChangePasswordForm(Form): get_form_field_label("retype_password"), render_kw={"autocomplete": "new-password"}, validators=[ - EqualTo("new_password", message="RETYPE_PASSWORD_MISMATCH"), + EqualToLocalize("new_password", message="RETYPE_PASSWORD_MISMATCH"), password_required, ], ) diff --git a/flask_security/recovery_codes.py b/flask_security/recovery_codes.py index 7f4d4693..56cf0583 100644 --- a/flask_security/recovery_codes.py +++ b/flask_security/recovery_codes.py @@ -22,7 +22,7 @@ get_form_field_label, get_form_field_xlate, Form, - Required, + RequiredLocalize, StringField, SubmitField, ) @@ -158,7 +158,7 @@ class MfRecoveryForm(Form): code = StringField( get_form_field_xlate(_("Recovery Code")), - validators=[Required()], + validators=[RequiredLocalize()], ) submit = SubmitField(get_form_field_label("submitcode")) diff --git a/flask_security/unified_signin.py b/flask_security/unified_signin.py index fdd39b32..5749e469 100644 --- a/flask_security/unified_signin.py +++ b/flask_security/unified_signin.py @@ -55,7 +55,7 @@ _setup_methods_xlate, Form, NextFormMixin, - Required, + RequiredLocalize, build_form_from_request, build_form, form_errors_munge, @@ -244,7 +244,7 @@ class UnifiedSigninForm(_UnifiedPassCodeForm, NextFormMixin): identity = StringField( get_form_field_label("identity"), - validators=[Required()], + validators=[RequiredLocalize()], ) remember = BooleanField(get_form_field_label("remember_me")) @@ -374,7 +374,7 @@ class UnifiedSigninSetupValidateForm(Form): "inputtype": "numeric", "pattern": "[0-9]*", }, - validators=[Required()], + validators=[RequiredLocalize()], ) submit = SubmitField(get_form_field_label("submitcode")) diff --git a/flask_security/webauthn.py b/flask_security/webauthn.py index f8baf5dd..1652a7db 100644 --- a/flask_security/webauthn.py +++ b/flask_security/webauthn.py @@ -77,7 +77,7 @@ from .decorators import anonymous_user_required, auth_required, unauth_csrf from .forms import ( Form, - Required, + RequiredLocalize, build_form_from_request, build_form, get_form_field_label, @@ -120,7 +120,7 @@ class WebAuthnRegisterForm(Form): name = StringField( get_form_field_xlate(_("Nickname")), - validators=[Required(message="WEBAUTHN_NAME_REQUIRED")], + validators=[RequiredLocalize(message="WEBAUTHN_NAME_REQUIRED")], ) usage = RadioField( get_form_field_xlate(_("Usage")), @@ -354,7 +354,7 @@ def validate(self, **kwargs: t.Any) -> bool: class WebAuthnDeleteForm(Form): name = StringField( get_form_field_xlate(_("Nickname")), - validators=[Required(message="WEBAUTHN_NAME_REQUIRED")], + validators=[RequiredLocalize(message="WEBAUTHN_NAME_REQUIRED")], ) submit = SubmitField(label=get_form_field_label("delete")) diff --git a/tests/conftest.py b/tests/conftest.py index d9d15ddb..406a763d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -115,9 +115,10 @@ def app(request): app.config["WTF_CSRF_ENABLED"] = False # Our test emails/domain isn't necessarily valid app.config["SECURITY_EMAIL_VALIDATOR_ARGS"] = {"check_deliverability": False} - app.config["SECURITY_TWO_FACTOR_SECRET"] = { + app.config["SECURITY_TOTP_SECRETS"] = { "1": "TjQ9Qa31VOrfEzuPy4VHQWPCTmRzCnFzMKLxXYiZu9B" } + app.config["SECURITY_TOTP_ISSUER"] = "tests" app.config["SECURITY_SMS_SERVICE"] = "test" app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False diff --git a/tests/test_misc.py b/tests/test_misc.py index c806ffd3..1045081f 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -50,7 +50,7 @@ PasswordlessLoginForm, RegisterForm, RegisterFormV2, - Required, + RequiredLocalize, ResetPasswordForm, SendConfirmationForm, StringField, @@ -488,7 +488,7 @@ def test_custom_form_setting(app, sqlalchemy_datastore): def test_form_required(app, sqlalchemy_datastore): class MyLoginForm(LoginForm): - myfield = StringField("My Custom Field", validators=[Required()]) + myfield = StringField("My Custom Field", validators=[RequiredLocalize()]) app.config["SECURITY_LOGIN_FORM"] = MyLoginForm @@ -508,7 +508,9 @@ def test_form_required_local_message(app, sqlalchemy_datastore): msg = "hi! did you forget me?" class MyLoginForm(LoginForm): - myfield = StringField("My Custom Field", validators=[Required(message=msg)]) + myfield = StringField( + "My Custom Field", validators=[RequiredLocalize(message=msg)] + ) app.config["SECURITY_LOGIN_FORM"] = MyLoginForm diff --git a/tests/test_two_factor.py b/tests/test_two_factor.py index 9b521d07..3f47ffb8 100644 --- a/tests/test_two_factor.py +++ b/tests/test_two_factor.py @@ -1239,7 +1239,7 @@ def test_authr_identity(app, client): 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_issuer"] == "tests" assert response.json["response"]["tf_authr_username"] == "jill" assert response.json["response"]["tf_state"] == "validating_profile" assert "tf_authr_key" in response.json["response"] diff --git a/tests/test_unified_signin.py b/tests/test_unified_signin.py index 15b09f88..87d71fcf 100644 --- a/tests/test_unified_signin.py +++ b/tests/test_unified_signin.py @@ -1754,7 +1754,7 @@ def test_totp_generation(app, client, get_message): "us-setup", json=dict(chosen_method="authenticator"), headers=headers ) assert response.status_code == 200 - assert response.json["response"]["authr_issuer"] == "service_name" + assert response.json["response"]["authr_issuer"] == "tests" assert response.json["response"]["authr_username"] == "dave@lp.com" assert "authr_key" in response.json["response"] diff --git a/tox.ini b/tox.ini index e2f5a7d4..8f4de749 100644 --- a/tox.ini +++ b/tox.ini @@ -163,7 +163,7 @@ deps = jinja2 skip_install = true commands = - pybabel extract --version 5.5.2 --keyword=_fsdomain --project=Flask-Security \ + pybabel extract --version 5.6.0 --keyword=_fsdomain --project=Flask-Security \ -o flask_security/translations/flask_security.pot \ --msgid-bugs-address=jwag956@github.com --mapping-file=babel.ini \ --add-comments=NOTE flask_security