Skip to content

Commit

Permalink
Chore for 5.6
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
jwag956 committed Jan 1, 2025
1 parent b70e12f commit b759a14
Show file tree
Hide file tree
Showing 15 changed files with 61 additions and 73 deletions.
10 changes: 8 additions & 2 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
+++++
Expand All @@ -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
-------------

Expand Down
2 changes: 1 addition & 1 deletion LICENSE.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 0 additions & 17 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions flask_security/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""

Expand Down Expand Up @@ -142,4 +142,4 @@
)
from .webauthn_util import WebauthnUtil

__version__ = "5.5.2"
__version__ = "5.6.0"
23 changes: 3 additions & 20 deletions flask_security/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -1548,6 +1540,7 @@ def init_app(
"recoverable",
"two_factor",
"unified_signin",
"username_recovery",
"passwordless",
"webauthn",
"mail_util_cls",
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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.
Expand Down
43 changes: 28 additions & 15 deletions flask_security/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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,
],
)
Expand All @@ -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,
Expand Down Expand Up @@ -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,
]
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(),
],
)

Expand Down Expand Up @@ -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,
],
)
Expand Down
4 changes: 2 additions & 2 deletions flask_security/recovery_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
get_form_field_label,
get_form_field_xlate,
Form,
Required,
RequiredLocalize,
StringField,
SubmitField,
)
Expand Down Expand Up @@ -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"))

Expand Down
6 changes: 3 additions & 3 deletions flask_security/unified_signin.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
_setup_methods_xlate,
Form,
NextFormMixin,
Required,
RequiredLocalize,
build_form_from_request,
build_form,
form_errors_munge,
Expand Down Expand Up @@ -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"))

Expand Down Expand Up @@ -374,7 +374,7 @@ class UnifiedSigninSetupValidateForm(Form):
"inputtype": "numeric",
"pattern": "[0-9]*",
},
validators=[Required()],
validators=[RequiredLocalize()],
)
submit = SubmitField(get_form_field_label("submitcode"))

Expand Down
6 changes: 3 additions & 3 deletions flask_security/webauthn.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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")),
Expand Down Expand Up @@ -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"))

Expand Down
3 changes: 2 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 5 additions & 3 deletions tests/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
PasswordlessLoginForm,
RegisterForm,
RegisterFormV2,
Required,
RequiredLocalize,
ResetPasswordForm,
SendConfirmationForm,
StringField,
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion tests/test_two_factor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
2 changes: 1 addition & 1 deletion tests/test_unified_signin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"] == "[email protected]"
assert "authr_key" in response.json["response"]

Expand Down
Loading

0 comments on commit b759a14

Please sign in to comment.