Skip to content

Commit

Permalink
Merge branch 'main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
jwag956 authored Feb 8, 2025
2 parents b1d666d + ca15fde commit 24d8515
Show file tree
Hide file tree
Showing 10 changed files with 78 additions and 10 deletions.
25 changes: 25 additions & 0 deletions flask_security/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,7 @@ def validate(self, **kwargs: t.Any) -> bool:
return False
assert self.user is not None
if self.user.confirmed_at is not None:
assert isinstance(self.email.errors, list)
self.email.errors.append(get_message("ALREADY_CONFIRMED")[0])
return False
return True
Expand All @@ -457,6 +458,7 @@ def validate(self, **kwargs: t.Any) -> bool:
if not super().validate(**kwargs):
return False
assert self.user is not None
assert isinstance(self.email.errors, list)
if not self.user.is_active:
self.email.errors.append(get_message("DISABLED_ACCOUNT")[0])
return False
Expand Down Expand Up @@ -486,6 +488,7 @@ def validate(self, **kwargs: t.Any) -> bool:
if not super().validate(**kwargs):
return False
assert self.user is not None
assert isinstance(self.email.errors, list)
if not self.user.is_active:
self.email.errors.append(get_message("DISABLED_ACCOUNT")[0])
return False
Expand Down Expand Up @@ -549,6 +552,7 @@ def validate(self, **kwargs: t.Any) -> bool:
if not super().validate(**kwargs):
return False

assert self.password.data is not None # validator password_required
# Stay clear of accessing 'username' unless we added that field.
# Lots of applications have added their own.
# To make subclassing easier - if self.ifield has been set we assume
Expand Down Expand Up @@ -578,9 +582,11 @@ def validate(self, **kwargs: t.Any) -> bool:
if uia_email:
self.ifield = self.email

assert isinstance(self.password.errors, list)
if self.user is None:
msg = get_message("USER_DOES_NOT_EXIST")[0]
if self.ifield:
assert isinstance(self.ifield.errors, list)
self.ifield.errors.append(msg)
else:
self.form_errors.append(msg)
Expand All @@ -602,6 +608,8 @@ def validate(self, **kwargs: t.Any) -> bool:
# to return detailed errors.
self.user_authenticated = True
self.requires_confirmation = requires_confirmation(self.user)
assert self.ifield is not None
assert isinstance(self.ifield.errors, list)
if self.requires_confirmation:
self.ifield.errors.append(get_message("CONFIRMATION_REQUIRED")[0])
return False
Expand Down Expand Up @@ -648,7 +656,9 @@ def validate(self, **kwargs: t.Any) -> bool:
if not super().validate(**kwargs): # pragma: no cover
return False

assert self.password.data is not None
self.password.data = _security._password_util.normalize(self.password.data)
assert isinstance(self.password.errors, list)
if not self.user.verify_and_update_password(self.password.data):
self.password.errors.append(get_message("INVALID_PASSWORD")[0])
return False
Expand Down Expand Up @@ -685,6 +695,7 @@ def validate(self, **kwargs: t.Any) -> bool:

# whether a password is required is a config variable (PASSWORD_REQUIRED).
# For unified signin there are many other ways to authenticate
assert isinstance(self.password.errors, list)
if cv("PASSWORD_REQUIRED"):
if not self.password.data or not self.password.data.strip():
self.password.errors.append(get_message("PASSWORD_NOT_PROVIDED")[0])
Expand Down Expand Up @@ -724,6 +735,7 @@ class RegisterForm(ConfirmRegisterForm, NextFormMixin):
def validate(self, **kwargs: t.Any) -> bool:
if not super().validate(**kwargs):
return False
assert isinstance(self.password_confirm.errors, list)
if not cv("UNIFIED_SIGNIN"):
# password_confirm required
if not self.password_confirm.data or not self.password_confirm.data.strip():
Expand Down Expand Up @@ -798,6 +810,7 @@ def validate(self, **kwargs: t.Any) -> bool:
if not super().validate(**kwargs):
failed = True

assert isinstance(self.password.errors, list)
if self.password.data:
# We do explicit validation here for passwords
# (rather than write a validator class) for 2 reasons:
Expand Down Expand Up @@ -862,6 +875,8 @@ def validate(self, **kwargs: t.Any) -> bool:
if not super().validate(**kwargs):
return False

assert isinstance(self.password.errors, list)
assert self.password.data is not None
pbad, self.password.data = _security._password_util.validate(
self.password.data, False, user=self.user
)
Expand Down Expand Up @@ -900,6 +915,8 @@ def validate(self, **kwargs: t.Any) -> bool:

# If user doesn't have a password then the caller (view) has already
# verified a current fresh session.
assert isinstance(self.password.errors, list)
assert isinstance(self.new_password.errors, list)
if current_user.password:
if not self.password.data or not self.password.data.strip():
self.password.errors.append(get_message("PASSWORD_NOT_PROVIDED")[0])
Expand All @@ -913,6 +930,7 @@ def validate(self, **kwargs: t.Any) -> bool:
self.password.errors.append(get_message("PASSWORD_IS_THE_SAME")[0])
return False

assert self.new_password.data is not None
pbad, self.new_password.data = _security._password_util.validate(
self.new_password.data, False, user=current_user
)
Expand Down Expand Up @@ -948,6 +966,8 @@ def validate(self, **kwargs: t.Any) -> bool:
if not super().validate(**kwargs): # pragma: no cover
return False
choices = list(cv("TWO_FACTOR_ENABLED_METHODS"))
assert isinstance(self.setup.errors, list)
assert isinstance(self.phone.errors, list)
if "email" in choices:
# backwards compat
choices.append("mail")
Expand All @@ -957,6 +977,9 @@ def validate(self, **kwargs: t.Any) -> bool:
self.setup.errors.append(get_message("TWO_FACTOR_METHOD_NOT_AVAILABLE")[0])
return False
if self.setup.data == "sms":
if not self.phone.data:
self.phone.errors.append(get_message("PHONE_INVALID")[0])
return False
msg = _security._phone_util.validate_phone_number(self.phone.data)
if msg:
self.phone.errors.append(msg)
Expand Down Expand Up @@ -995,6 +1018,8 @@ def validate(self, **kwargs: t.Any) -> bool:

# verify entered code with user's totp secret
assert self.user is not None
assert self.code.data is not None
assert isinstance(self.code.errors, list)
if not _security._totp_factory.verify_totp(
token=self.code.data,
totp_secret=self.tf_totp_secret,
Expand Down
2 changes: 2 additions & 0 deletions flask_security/recovery_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ def validate(self, **kwargs: t.Any) -> bool:
if not super().validate(**kwargs): # pragma: no cover
return False
assert self.user is not None
assert self.code.data is not None # RequiredLocalize validator
assert isinstance(self.code.errors, list)
if not _security._mf_recovery_codes_util.check_recovery_code(
self.user, self.code.data
):
Expand Down
2 changes: 1 addition & 1 deletion flask_security/tf_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def tf_select() -> ResponseValue:
return tf_illegal_state(form, cv("TWO_FACTOR_ERROR_VIEW"))

setup_methods = _security.two_factor_plugins.get_setup_tf_methods(user)
form.which.choices = setup_methods
form.which.choices = setup_methods # type: ignore[assignment]

if form.validate_on_submit():
response = None
Expand Down
2 changes: 2 additions & 0 deletions flask_security/twofactor.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ def set_rescue_options(form: TwoFactorRescueForm, user: UserMixin) -> dict[str,

if cv("TWO_FACTOR_RESCUE_EMAIL"):
recovery_options["email"] = url_for_security("two_factor_rescue")
assert isinstance(form.help_setup.choices, list)
form.help_setup.choices.append(
("email", get_form_field_xlate(_("Send code via email")))
)
Expand All @@ -141,6 +142,7 @@ def set_rescue_options(form: TwoFactorRescueForm, user: UserMixin) -> dict[str,
and _datastore.mf_get_recovery_codes(user)
):
recovery_options["recovery_code"] = url_for_security("mf_recovery")
assert isinstance(form.help_setup.choices, list)
form.help_setup.choices.append(
(
"recovery_code",
Expand Down
24 changes: 19 additions & 5 deletions flask_security/unified_signin.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ def validate2(self) -> bool:
# Since we have a unique totp_secret for each method - we
# can figure out which mechanism was used.
# Note that password check requires a string (not int or None)
assert isinstance(self.passcode.errors, list)
passcode = self.passcode.data
if not passcode:
self.passcode.errors.append(get_message("INVALID_PASSWORD_CODE")[0])
Expand Down Expand Up @@ -218,6 +219,7 @@ def validate2(self) -> bool:
return True
elif self.submit_send_code.data:
# Send a code - chosen_method must be valid
assert isinstance(self.chosen_method.errors, list)
cm = self.chosen_method.data
if cm not in cv("US_ENABLED_METHODS"):
self.chosen_method.errors.append(
Expand Down Expand Up @@ -263,6 +265,7 @@ def validate(self, **kwargs: t.Any) -> bool:

# Can't authenticate nor get a code if still required confirmation.
self.requires_confirmation = requires_confirmation(self.user)
assert isinstance(self.identity.errors, list)
if self.requires_confirmation:
self.identity.errors.append(get_message("CONFIRMATION_REQUIRED")[0])
return False
Expand Down Expand Up @@ -322,6 +325,10 @@ def validate(self, **kwargs: t.Any) -> bool:
if not super().validate(**kwargs):
return False

assert isinstance(self.chosen_method.errors, list)
assert isinstance(self.phone.errors, list)
assert isinstance(self.delete_method.errors, list)

if not self.chosen_method.data and not self.delete_method.data:
self.form_errors.append(get_message("API_ERROR")[0])
return False
Expand All @@ -333,6 +340,9 @@ def validate(self, **kwargs: t.Any) -> bool:
return False

if self.chosen_method.data == "sms":
if not self.phone.data:
self.phone.errors.append(get_message("PHONE_INVALID")[0])
return False
msg = _security._phone_util.validate_phone_number(self.phone.data)
if msg:
self.phone.errors.append(msg)
Expand Down Expand Up @@ -385,6 +395,8 @@ def validate(self, **kwargs: t.Any) -> bool:
if not super().validate(**kwargs):
return False

assert isinstance(self.passcode.errors, list)
assert self.passcode.data is not None # RequiredLocalize validator
if not _security._totp_factory.verify_totp(
token=self.passcode.data,
totp_secret=self.totp_secret,
Expand Down Expand Up @@ -429,6 +441,7 @@ def us_signin_send_code() -> ResponseValue:
if form.validate_on_submit():
msg = _send_code_helper(form, True)
if msg:
assert isinstance(form.chosen_method.errors, list)
form.chosen_method.errors.append(msg)

if _security._want_json(request):
Expand Down Expand Up @@ -500,6 +513,7 @@ def us_verify_send_code() -> ResponseValue:
if form.validate_on_submit():
msg = _send_code_helper(form, False)
if msg:
assert isinstance(form.chosen_method.errors, list)
form.chosen_method.errors.append(msg)

if _security._want_json(request):
Expand Down Expand Up @@ -829,11 +843,10 @@ def us_setup() -> ResponseValue:
# N.B. totp (totp_secret) is actually encrypted - so it seems safe enough
# to send it to the user.
# Only check phone number if SMS (see form validate)
phone_number = (
_security._phone_util.get_canonical_form(form.phone.data)
if add_method == "sms"
else None
)
phone_number = None
if add_method == "sms":
assert form.phone.data is not None
phone_number = _security._phone_util.get_canonical_form(form.phone.data)
state = {
"totp_secret": totp,
"chosen_method": add_method,
Expand All @@ -846,6 +859,7 @@ def us_setup() -> ResponseValue:
)
if msg:
# sending didn't work.
assert isinstance(form.chosen_method.errors, list)
form.chosen_method.errors.append(msg)
if _security._want_json(request):
# Not authenticated yet - so don't send any user info.
Expand Down
1 change: 1 addition & 0 deletions flask_security/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ def login() -> ResponseValue:
):
# Validation failed BECAUSE user needs to confirm
assert form.user_authenticated
assert form.email.data # email_required validator
do_flash(*get_message("CONFIRMATION_REQUIRED"))
return redirect(
get_url(
Expand Down
14 changes: 12 additions & 2 deletions flask_security/webauthn.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Flask-Security WebAuthn module
:copyright: (c) 2021-2024 by J. Christopher Wagner (jwag).
:copyright: (c) 2021-2025 by J. Christopher Wagner (jwag).
:license: MIT, see LICENSE for more details.
This implements support for webauthn/FIDO2 Level 2 using the py_webauthn package.
Expand Down Expand Up @@ -139,6 +139,7 @@ class WebAuthnRegisterForm(Form):
def validate(self, **kwargs: t.Any) -> bool:
if not super().validate(**kwargs):
return False
assert isinstance(self.name.errors, list)
inuse = any([self.name.data == cred.name for cred in current_user.webauthn])
if inuse:
msg = get_message("WEBAUTHN_NAME_INUSE", name=self.name.data)[0]
Expand Down Expand Up @@ -166,6 +167,10 @@ class WebAuthnRegisterResponseForm(Form):
def validate(self, **kwargs: t.Any) -> bool:
if not super().validate(**kwargs):
return False # pragma: no cover
assert isinstance(self.credential.errors, list)
if not self.credential.data:
self.credential.errors.append(get_message("API_ERROR")[0])
return False
inuse = any([self.name == cred.name for cred in current_user.webauthn])
if inuse:
msg = get_message("WEBAUTHN_NAME_INUSE", name=self.name)[0]
Expand Down Expand Up @@ -267,6 +272,10 @@ class WebAuthnSigninResponseForm(Form, NextFormMixin):
def validate(self, **kwargs: t.Any) -> bool:
if not super().validate(**kwargs):
return False # pragma: no cover
assert isinstance(self.credential.errors, list)
if not self.credential.data:
self.credential.errors.append(get_message("API_ERROR")[0])
return False
try:
auth_cred = parse_authentication_credential_json(self.credential.data)
except (
Expand Down Expand Up @@ -364,6 +373,7 @@ class WebAuthnDeleteForm(Form):
def validate(self, **kwargs: t.Any) -> bool:
if not super().validate(**kwargs):
return False
assert isinstance(self.name.errors, list)
if not any([self.name.data == cred.name for cred in current_user.webauthn]):
self.name.errors.append(
get_message("WEBAUTHN_NAME_NOT_FOUND", name=self.name.data)[0]
Expand Down Expand Up @@ -718,7 +728,7 @@ def webauthn_signin_response(token: str) -> ResponseValue:
# - Did this credential provide 2-factor and
# is WAN_ALLOW_AS_MULTI_FACTOR set
# - Is another 2FA setup?
remember_me = form.remember.data if "remember" in form else None
remember_me = bool(form.remember.data)
if form.mf_check and cv("WAN_ALLOW_AS_MULTI_FACTOR"):
pass
else:
Expand Down
2 changes: 2 additions & 0 deletions tests/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,7 @@ def __init__(self, *args, service=None, **kwargs):
def validate(self, **kwargs: t.Any) -> bool:
if not super().validate(**kwargs): # pragma: no cover
return False
assert isinstance(self.email.errors, list)
if not self.myservice(self.email.data):
self.email.errors.append("Not happening")
return False
Expand Down Expand Up @@ -444,6 +445,7 @@ def instantiator(self, form_name, form_cls, *args, **kwargs):
def validate(self, **kwargs: t.Any) -> bool:
if not super().validate(**kwargs): # pragma: no cover
return False
assert isinstance(self.email.errors, list)
if not self.myservice(self.email.data):
self.email.errors.append("Not happening")
return False
Expand Down
4 changes: 3 additions & 1 deletion tests/test_unified_signin.py
Original file line number Diff line number Diff line change
Expand Up @@ -738,7 +738,9 @@ def test_setup(app, clients, get_message):
assert get_message("PHONE_INVALID") in response.data

# test invalid phone
response = client.post("us-setup", data=dict(chosen_method="sms", phone="555-1212"))
response = client.post(
"us-setup", data=dict(chosen_method="sms", phone="NOT-A-NUMBER")
)
assert response.status_code == 200
assert get_message("PHONE_INVALID") in response.data
assert b"Enter code here to complete setup" not in response.data
Expand Down
12 changes: 11 additions & 1 deletion tests/test_webauthn.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
WebAuthn tests
:copyright: (c) 2021-2024 by J. Christopher Wagner (jwag).
:copyright: (c) 2021-2025 by J. Christopher Wagner (jwag).
:license: MIT, see LICENSE for more details.
"""
Expand Down Expand Up @@ -542,6 +542,11 @@ def test_bad_data_register(app, client, get_message):
assert response.json["response"]["errors"][0].encode("utf-8") == get_message(
"API_ERROR"
)
response = client.post(response_url, json=dict(credential=""))
assert response.status_code == 400
assert response.json["response"]["errors"][0].encode("utf-8") == get_message(
"API_ERROR"
)

# Now pass incorrect keys
bad_register = copy.deepcopy(REG_DATA1)
Expand Down Expand Up @@ -590,6 +595,11 @@ def test_bad_data_signin(app, client, get_message):
assert response.json["response"]["errors"][0].encode("utf-8") == get_message(
"API_ERROR"
)
response = client.post(response_url, json=dict(credential=""))
assert response.status_code == 400
assert response.json["response"]["errors"][0].encode("utf-8") == get_message(
"API_ERROR"
)

# Now pass incorrect keys
bad_signin = copy.deepcopy(SIGNIN_DATA1)
Expand Down

0 comments on commit 24d8515

Please sign in to comment.