codekingpro/portable-devtools
114k
1"""2 flask_security.forms3 ~~~~~~~~~~~~~~~~~~~~4 5 Flask-Security forms module6 7 :copyright: (c) 2012 by Matt Wright.8 :copyright: (c) 2017 by CERN.9 :copyright: (c) 2019-2024 by J. Christopher Wagner (jwag).10 :license: MIT, see LICENSE for more details.11"""12 13from __future__ import annotations14 15import inspect16import typing as t17 18from flask import current_app, request19from flask_login import current_user20from flask_wtf import FlaskForm as BaseForm21from markupsafe import Markup22from wtforms import (23 BooleanField,24 EmailField,25 Field,26 HiddenField,27 PasswordField,28 RadioField,29 StringField,30 SubmitField,31 TelField,32 ValidationError,33 validators,34)35 36from werkzeug.datastructures import MultiDict37from wtforms.validators import Optional, StopValidation38 39from .babel import is_lazy_string, make_lazy_string40from .confirmable import requires_confirmation41from .mail_util import EmailValidateException42from .proxies import _security43from .utils import (44 _,45 _datastore,46 config_value as cv,47 do_flash,48 get_identity_attribute,49 get_message,50 hash_password,51 localize_callback,52 suppress_form_csrf,53 url_for_security,54 validate_redirect_url,55 verify_password,56)57 58if t.TYPE_CHECKING: # pragma: no cover59 from .datastore import User60 61_default_field_labels = {62 "email": _("Email Address"),63 "password": _("Password"),64 "remember_me": _("Remember Me"),65 "login": _("Login"),66 "signin": _("Sign In"),67 "register": _("Register"),68 "send_confirmation": _("Resend Confirmation Instructions"),69 "recover_password": _("Recover Password"),70 "reset_password": _("Reset Password"),71 "retype_password": _("Retype Password"),72 "new_password": _("New Password"),73 "change_password": _("Change Password"),74 "send_login_link": _("Send Login Link"),75 "verify_password": _("Verify Password"),76 "change_method": _("Change Method"),77 "phone": _("Phone Number"),78 "code": _("Authentication Code"),79 "submit": _("Submit"),80 "submitcode": _("Submit Code"),81 "error": _("Error(s)"),82 "identity": _("Identity"),83 "sendcode": _("Send Code"),84 "passcode": _("Passcode"),85 "username": _("Username"),86 "delete": _("Delete"),87 "email_method": _("Set up using email"),88 "authapp_method": _(89 "Set up using an authenticator app (e.g. google, lastpass, authy)"90 ),91 "sms_method": _("Set up using SMS"),92}93 94# translated methods for two-factor and us-signin. keyed by form 'choices'95_setup_methods_xlate = {96 "google_authenticator": _("Google Authenticator"),97 "authenticator": _("authenticator"),98 "email": _("email"),99 "mail": _("email"),100 "sms": _("SMS"),101 "password": _("password"),102 None: _("none"),103}104 105 106class ValidatorMixin:107 """108 This is called at import time - so there is no app context.109 Validators have state - namely self.message - but we need that110 xlated on a per-request basis. So we want a lazy_string - but we can't create111 that until we are in an app context.112 """113 114 def __init__(self, *args, **kwargs):115 # If the message is available from config[MSG_xx] then it can be xlated.116 # Otherwise it will be used as is.117 if "message" in kwargs:118 self._original_message = kwargs["message"]119 del kwargs["message"]120 else:121 self._original_message = None122 super().__init__(*args, **kwargs)123 124 def __call__(self, form, field):125 if self._original_message and (126 not is_lazy_string(self.message) and not self.message127 ):128 # Creat on first usage within app context.129 msg = cv("MSG_" + self._original_message, strict=False)130 if msg:131 self.message = make_lazy_string(_local_xlate, msg[0])132 else:133 self.message = self._original_message134 return super().__call__(form, field)135 136 137class EqualTo(ValidatorMixin, validators.EqualTo):138 pass139 140 141class Required(ValidatorMixin, validators.DataRequired):142 pass143 144 145class Length(ValidatorMixin, validators.Length):146 pass147 148 149class EmailValidation:150 """Simple interface to email_validator.151 N.B. Side-effect - if valid email, the field.data is set to the normalized value.152 153 The 'verify' keyword informs the validator to perform checks to be more sure154 that the email can actually receive an email (as well as normalize).155 Set to False - just normalize (for use with identity purposes).156 """157 158 def __init__(self, *args, **kwargs):159 self.verify = kwargs.get("verify", False)160 161 def __call__(self, form, field):162 if field.data is None: # pragma: no cover163 raise ValidationError(get_message("EMAIL_NOT_PROVIDED")[0])164 165 try:166 if self.verify:167 field.data = _security._mail_util.validate(field.data)168 else:169 field.data = _security._mail_util.normalize(field.data)170 except EmailValidateException as e:171 # we stop further validators if email isn't valid.172 # TODO: email_validator provides some really nice error messages - however173 # they aren't localized. And there isn't an easy way to add multiple174 # errors at once.175 raise StopValidation(e.msg)176 except ValueError:177 # Backwards compat - mail_util no longer raises this - but app subclasses178 # might (and we're making this change in 5.4.3).179 msg = get_message("INVALID_EMAIL_ADDRESS")[0]180 raise StopValidation(msg)181 182 183email_required = Required(message="EMAIL_NOT_PROVIDED")184password_required = Required(message="PASSWORD_NOT_PROVIDED")185 186 187def _local_xlate(text):188 """LazyStrings need to be evaluated in the context of a request189 where _security.i18_domain is available.190 """191 return localize_callback(text)192 193 194def get_form_field_label(key):195 """This is called during import since form fields are declared as part of196 class. Thus can't call 'localize_callback' until we need to actually197 translate/render form.198 """199 return make_lazy_string(_local_xlate, _default_field_labels.get(key, ""))200 201 202def get_form_field_xlate(txt):203 return make_lazy_string(_local_xlate, txt)204 205 206def valid_user_email(form, field):207 # Verify email exists in DB - be sure to normalize first.208 # Side-effect - set form.user if field is valid209 uia_email = get_identity_attribute("email")210 form.user = _datastore.find_user(211 case_insensitive=uia_email.get("case_insensitive", False), email=field.data212 )213 if form.user is None:214 raise ValidationError(get_message("USER_DOES_NOT_EXIST")[0])215 216 217def unique_user_email(form, field):218 # Verify email not already in DB219 # Assumes field value already normalized - email_validator does this.220 uia_email = get_identity_attribute("email")221 form.existing_email_user = _datastore.find_user(222 case_insensitive=uia_email.get("case_insensitive", False), email=field.data223 )224 if form.existing_email_user is not None:225 msg = get_message("EMAIL_ALREADY_ASSOCIATED", email=field.data)[0]226 raise ValidationError(msg)227 228 229def username_validator(form, field):230 # Side-effect - field.data is updated to normalized value.231 msg, field.data = _security._username_util.validate(field.data)232 if msg:233 raise ValidationError(msg)234 235 236def unique_username(form, field):237 # Verify username not already in DB238 # Assumes field value already normalized - username_validator does this.239 uia_username = get_identity_attribute("username")240 form.existing_username_user = _datastore.find_user(241 case_insensitive=uia_username.get("case_insensitive", False),242 username=field.data,243 )244 if form.existing_username_user is not None:245 msg = get_message("USERNAME_ALREADY_ASSOCIATED", username=field.data)[0]246 raise ValidationError(msg)247 248 249def unique_identity_attribute(form, field):250 """A validator that checks the field data against all configured251 :py:data:`SECURITY_USER_IDENTITY_ATTRIBUTES`.252 This can be used as part of registration.253 254 Be aware that the "mapper" function likely also normalizes the input in addition255 to validating it.256 257 :param form:258 :param field:259 :return: Nothing; if field data corresponds to an existing User, ValidationError260 is raised.261 """262 for mapping in cv("USER_IDENTITY_ATTRIBUTES"):263 attr = list(mapping.keys())[0]264 details = mapping[attr]265 idata = details["mapper"](field.data)266 if idata:267 if _datastore.find_user(268 case_insensitive=details.get("case_insensitive", False), **{attr: idata}269 ):270 msg = get_message(271 "IDENTITY_ALREADY_ASSOCIATED", attr=attr, value=idata272 )[0]273 raise ValidationError(msg)274 275 276class Form(BaseForm):277 def __init__(self, *args, **kwargs):278 if current_app and current_app.testing:279 self.TIME_LIMIT = None280 super().__init__(*args, **kwargs)281 282 283def generic_message(284 detailed_msg: str, generic_msg: str, **kwargs: t.Any285) -> tuple[str, str]:286 if cv("RETURN_GENERIC_RESPONSES"):287 m, c = get_message(generic_msg, **kwargs)288 else:289 m, c = get_message(detailed_msg, **kwargs)290 return m, c291 292 293def form_errors_munge(form: Form, fields: dict[str, dict[str, str]]) -> None:294 """295 To support OWASP best practice on unauthenticated endpoints to avoid296 disclosing whether a user exists or not we need to return generic error messages.297 Furthermore, WTForms really likes to place errors on the field itself - which is298 a dead giveaway. We need to move errors from fields to the form.form_errors, and299 (optionally) replace then with generic msgs.300 """301 if not cv("RETURN_GENERIC_RESPONSES"): # pragma: no cover302 return303 304 for fname, rinfo in fields.items():305 field = getattr(form, fname)306 if field.errors:307 field.errors = []308 # If they want to replace that message with a generic message and place309 # it in the generic/form level errors - do that.310 if replace_msg := rinfo.get("replace_msg"):311 form.form_errors.append(get_message(replace_msg)[0])312 313 314class UserEmailFormMixin:315 email = EmailField(316 get_form_field_label("email"),317 render_kw={"autocomplete": "email"},318 validators=[email_required, EmailValidation(verify=True), valid_user_email],319 )320 321 322class UniqueEmailFormMixin:323 email = EmailField(324 get_form_field_label("email"),325 render_kw={"autocomplete": "email"},326 validators=[email_required, EmailValidation(verify=True), unique_user_email],327 )328 329 330class PasswordFormMixin:331 password = PasswordField(332 get_form_field_label("password"),333 render_kw={"autocomplete": "current-password"},334 validators=[password_required],335 )336 337 338class NewPasswordFormMixin:339 password = PasswordField(340 get_form_field_label("password"),341 render_kw={"autocomplete": "new-password"},342 validators=[password_required],343 )344 345 346class PasswordConfirmFormMixin:347 password_confirm = PasswordField(348 get_form_field_label("retype_password"),349 render_kw={"autocomplete": "new-password"},350 validators=[351 EqualTo("password", message="RETYPE_PASSWORD_MISMATCH"),352 password_required,353 ],354 )355 356 357class NextFormMixin:358 next = HiddenField()359 360 def validate_next(self, field):361 if field.data and not validate_redirect_url(field.data):362 field.data = ""363 do_flash(*get_message("INVALID_REDIRECT"))364 raise ValidationError(get_message("INVALID_REDIRECT")[0])365 366 367class CodeFormMixin:368 code = StringField(369 get_form_field_label("code"),370 render_kw={371 "autocomplete": "one-time-code",372 "inputtype": "numeric",373 "pattern": "[0-9]*",374 },375 validators=[Required()],376 )377 378 379def get_register_username_field(app):380 if cv("USERNAME_REQUIRED", app=app):381 validators = [382 Required(message="USERNAME_NOT_PROVIDED"),383 username_validator,384 unique_username,385 ]386 else:387 validators = [username_validator, unique_username]388 return StringField(389 get_form_field_label("username"),390 render_kw={"autocomplete": "username"},391 validators=validators,392 )393 394 395login_username_field = StringField(396 get_form_field_label("username"),397 render_kw={"autocomplete": "username"},398 validators=[username_validator],399)400 401 402class RegisterFormMixin:403 submit = SubmitField(get_form_field_label("register"))404 405 # The "username" field is added in init_app if USERNAME_ENABLE is set.406 # This is just a type hint.407 username: t.ClassVar[Field]408 409 def to_dict(self, only_user):410 """411 Return form data as dictionary412 :param only_user: bool, if True then only fields that have413 corresponding members in UserModel are returned414 :return: dict415 """416 417 def is_field_and_user_attr(member):418 if not isinstance(member, Field):419 return False420 421 # If only fields recorded on UserModel should be returned,422 # perform check on user model, else return True423 if only_user is True:424 return hasattr(_datastore.user_model, member.name)425 else:426 return True427 428 fields = inspect.getmembers(self, is_field_and_user_attr)429 return {key: value.data for key, value in fields}430 431 432class SendConfirmationForm(Form, UserEmailFormMixin):433 """The default send confirmation form"""434 435 submit = SubmitField(get_form_field_label("send_confirmation"))436 437 def __init__(self, *args: t.Any, **kwargs: t.Any):438 super().__init__(*args, **kwargs)439 self.user: User | None = None # set by valid_user_email440 if request and request.method == "GET":441 self.email.data = request.args.get("email", None)442 443 def validate(self, **kwargs: t.Any) -> bool:444 if not super().validate(**kwargs):445 return False446 assert self.user is not None447 if self.user.confirmed_at is not None:448 self.email.errors.append(get_message("ALREADY_CONFIRMED")[0])449 return False450 return True451 452 453class ForgotPasswordForm(Form, UserEmailFormMixin):454 """The default forgot password form"""455 456 submit = SubmitField(get_form_field_label("recover_password"))457 458 def __init__(self, *args: t.Any, **kwargs: t.Any):459 super().__init__(*args, **kwargs)460 self.requires_confirmation: bool = False461 self.user: User | None = None # set by valid_user_email462 463 def validate(self, **kwargs: t.Any) -> bool:464 if not super().validate(**kwargs):465 return False466 assert self.user is not None467 if not self.user.is_active:468 self.email.errors.append(get_message("DISABLED_ACCOUNT")[0])469 return False470 self.requires_confirmation = requires_confirmation(self.user)471 if self.requires_confirmation:472 self.email.errors.append(get_message("CONFIRMATION_REQUIRED")[0])473 return False474 return True475 476 477class PasswordlessLoginForm(Form):478 """The passwordless login form"""479 480 email = EmailField(481 get_form_field_label("email"),482 render_kw={"autocomplete": "email"},483 validators=[email_required, EmailValidation(verify=False), valid_user_email],484 )485 486 submit = SubmitField(get_form_field_label("send_login_link"))487 488 def __init__(self, *args: t.Any, **kwargs: t.Any):489 super().__init__(*args, **kwargs)490 self.user: User | None = None # set by valid_user_email491 492 def validate(self, **kwargs: t.Any) -> bool:493 if not super().validate(**kwargs):494 return False495 assert self.user is not None496 if not self.user.is_active:497 self.email.errors.append(get_message("DISABLED_ACCOUNT")[0])498 return False499 return True500 501 502class LoginForm(Form, PasswordFormMixin, NextFormMixin):503 """The default login form"""504 505 # email field - we don't use valid_user_email since for login506 # with username feature it is potentially optional.507 email = EmailField(508 get_form_field_label("email"),509 render_kw={"autocomplete": "email"},510 validators=[Optional(), EmailValidation(verify=False)],511 )512 513 # username is added dynamically based on USERNAME_ENABLED.514 username: t.ClassVar[Field]515 remember = BooleanField(get_form_field_label("remember_me"))516 submit = SubmitField(get_form_field_label("login"))517 518 def __init__(self, *args: t.Any, **kwargs: t.Any):519 super().__init__(*args, **kwargs)520 if request and not self.next.data:521 self.next.data = request.args.get("next", "")522 self.remember.default = cv("DEFAULT_REMEMBER_ME")523 if _security.recoverable and not self.password.description:524 html = Markup(525 f'<a href="{url_for_security("forgot_password")}">'526 f'{get_message("FORGOT_PASSWORD")[0]}</a>'527 )528 self.password.description = html529 self.requires_confirmation: bool = False530 self.user: User | None = None531 # ifield can be set by subclasses to skip identity checks.532 self.ifield: Field | None = None533 # If True then user has authenticated so we can show detailed errors534 self.user_authenticated = False535 536 def validate(self, **kwargs: t.Any) -> bool:537 if not super().validate(**kwargs):538 return False539 540 # Stay clear of accessing 'username' unless we added that field.541 # Lots of applications have added their own.542 # To make subclassing easier - if self.ifield has been set we assume543 # subclass has validated and attempted to look up user. It is also544 # responsible to deal with USER_IDENTITY_ATTRIBUTES if it cares.545 if not self.ifield:546 uia_email = get_identity_attribute("email")547 if uia_email and self.email.data:548 self.ifield = self.email549 self.user = _datastore.find_user(550 case_insensitive=uia_email.get("case_insensitive", False),551 email=self.email.data,552 )553 elif cv("USERNAME_ENABLE"):554 uia_username = get_identity_attribute("username")555 if uia_username and self.username.data:556 self.user = _datastore.find_user(557 case_insensitive=uia_username.get("case_insensitive", False),558 username=self.username.data,559 )560 self.ifield = self.username561 else:562 # A bit of backwards compat - the old LoginForm just had email and563 # any errors would be set on that field.564 if uia_email:565 self.ifield = self.email566 567 if self.user is None:568 msg = get_message("USER_DOES_NOT_EXIST")[0]569 if self.ifield:570 self.ifield.errors.append(msg)571 else:572 self.form_errors.append(msg)573 # Reduce timing variation between existing and non-existing users574 hash_password(self.password.data)575 return False576 if not self.user.password:577 # This is result of PASSWORD_REQUIRED=False and UNIFIED_SIGNIN578 self.password.errors.append(get_message("INVALID_PASSWORD")[0])579 # Reduce timing variation between existing and non-existing users580 hash_password(self.password.data)581 return False582 self.password.data = _security._password_util.normalize(self.password.data)583 if not self.user.verify_and_update_password(self.password.data):584 self.password.errors.append(get_message("INVALID_PASSWORD")[0])585 return False586 587 # At this point the user has successfully authenticated - so it is fine588 # to return detailed errors.589 self.user_authenticated = True590 self.requires_confirmation = requires_confirmation(self.user)591 if self.requires_confirmation:592 self.ifield.errors.append(get_message("CONFIRMATION_REQUIRED")[0])593 return False594 if not self.user.is_active:595 self.ifield.errors.append(get_message("DISABLED_ACCOUNT")[0])596 return False597 return True598 599 600class VerifyForm(Form, PasswordFormMixin):601 """The verify authentication form"""602 603 submit = SubmitField(get_form_field_label("verify_password"))604 605 def __init__(self, *args: t.Any, user: User, **kwargs: t.Any):606 super().__init__(*args, **kwargs)607 self.user: User = user608 609 def validate(self, **kwargs: t.Any) -> bool:610 if not super().validate(**kwargs): # pragma: no cover611 return False612 613 self.password.data = _security._password_util.normalize(self.password.data)614 if not self.user.verify_and_update_password(self.password.data):615 self.password.errors.append(get_message("INVALID_PASSWORD")[0])616 return False617 return True618 619 620class ConfirmRegisterForm(Form, RegisterFormMixin, UniqueEmailFormMixin):621 """This form is used for registering when 'confirmable' is set.622 The only difference between this and the other RegisterForm is that623 this one doesn't require re-typing in the password...624 625 We want to support OWASP best-practice around mitigating user enumeration.626 To that end we run through the entire validation regardless - this allows us627 to still return important bad-password messages.628 In the case of an existing email or username - we set form.existing_xx so that629 the view can decide how to match responses (e.g. json responses always return 200).630 """631 632 # Password optional when Unified Signin enabled.633 password = PasswordField(634 get_form_field_label("password"),635 render_kw={"autocomplete": "new-password"},636 )637 638 def __init__(self, *args, **kwargs):639 super().__init__(*args, **kwargs)640 self.existing_username_user = None641 self.existing_email_user = None642 643 def validate(self, **kwargs: t.Any) -> bool:644 failed = False645 if not super().validate(**kwargs):646 failed = True647 648 # whether a password is required is a config variable (PASSWORD_REQUIRED).649 # For unified signin there are many other ways to authenticate650 if cv("PASSWORD_REQUIRED") or not cv("UNIFIED_SIGNIN"):651 if not self.password.data or not self.password.data.strip():652 self.password.errors.append(get_message("PASSWORD_NOT_PROVIDED")[0])653 failed = True654 655 if self.password.data:656 # We do explicit validation here for passwords657 # (rather than write a validator class) for 2 reasons:658 # 1) We want to control which fields are passed -659 # sometimes that's current_user660 # other times it's the registration fields.661 # 2) We want to be able to return multiple error messages.662 rfields = {}663 for k, v in self.data.items():664 if hasattr(_datastore.user_model, k):665 rfields[k] = v666 del rfields["password"]667 pbad, self.password.data = _security._password_util.validate(668 self.password.data, True, **rfields669 )670 if pbad:671 self.password.errors.extend(pbad)672 failed = True673 return not failed674 675 676class RegisterForm(ConfirmRegisterForm, NextFormMixin):677 # Password optional when Unified Signin enabled.678 password_confirm = PasswordField(679 get_form_field_label("retype_password"),680 validators=[681 EqualTo("password", message="RETYPE_PASSWORD_MISMATCH"),682 validators.Optional(),683 ],684 )685 686 def validate(self, **kwargs: t.Any) -> bool:687 if not super().validate(**kwargs):688 return False689 if not cv("UNIFIED_SIGNIN"):690 # password_confirm required691 if not self.password_confirm.data or not self.password_confirm.data.strip():692 self.password_confirm.errors.append(693 get_message("PASSWORD_NOT_PROVIDED")[0]694 )695 return False696 return True697 698 def __init__(self, *args, **kwargs):699 super().__init__(*args, **kwargs)700 if not self.next.data:701 self.next.data = request.args.get("next", "")702 703 704class ResetPasswordForm(Form, NewPasswordFormMixin, PasswordConfirmFormMixin):705 """The default reset password form"""706 707 # filled in by caller708 user: User709 710 submit = SubmitField(get_form_field_label("reset_password"))711 712 def validate(self, **kwargs: t.Any) -> bool:713 if not super().validate(**kwargs):714 return False715 716 pbad, self.password.data = _security._password_util.validate(717 self.password.data, False, user=self.user718 )719 if pbad:720 self.password.errors.extend(pbad)721 return False722 return True723 724 725class ChangePasswordForm(Form):726 """The default change password form"""727 728 password = PasswordField(729 get_form_field_label("password"), render_kw={"autocomplete": "current-password"}730 )731 new_password = PasswordField(732 get_form_field_label("new_password"),733 render_kw={"autocomplete": "new-password"},734 validators=[password_required],735 )736 737 new_password_confirm = PasswordField(738 get_form_field_label("retype_password"),739 render_kw={"autocomplete": "new-password"},740 validators=[741 EqualTo("new_password", message="RETYPE_PASSWORD_MISMATCH"),742 password_required,743 ],744 )745 746 submit = SubmitField(get_form_field_label("change_password"))747 748 def validate(self, **kwargs: t.Any) -> bool:749 if not super().validate(**kwargs):750 return False751 752 # If user doesn't have a password then the caller (view) has already753 # verified a current fresh session.754 if current_user.password:755 if not self.password.data or not self.password.data.strip():756 self.password.errors.append(get_message("PASSWORD_NOT_PROVIDED")[0])757 return False758 759 self.password.data = _security._password_util.normalize(self.password.data)760 if not verify_password(self.password.data, current_user.password):761 self.password.errors.append(get_message("INVALID_PASSWORD")[0])762 return False763 if self.password.data == self.new_password.data:764 self.password.errors.append(get_message("PASSWORD_IS_THE_SAME")[0])765 return False766 767 pbad, self.new_password.data = _security._password_util.validate(768 self.new_password.data, False, user=current_user769 )770 if pbad:771 self.new_password.errors.extend(pbad)772 return False773 return True774 775 776class TwoFactorSetupForm(Form):777 """The Two-factor token validation form"""778 779 setup = RadioField(780 get_form_field_xlate(_("Available Methods")),781 choices=[782 ("disable", get_form_field_xlate(_("Disable two factor authentication"))),783 ("email", get_form_field_label("email_method")),784 (785 "authenticator",786 get_form_field_label("authapp_method"),787 ),788 ("sms", get_form_field_label("sms_method")),789 ],790 validate_choice=False,791 )792 phone = TelField(get_form_field_label("phone"))793 submit = SubmitField(get_form_field_label("submit"))794 795 def __init__(self, *args, **kwargs):796 super().__init__(*args, **kwargs)797 798 def validate(self, **kwargs: t.Any) -> bool:799 if not super().validate(**kwargs): # pragma: no cover800 return False801 choices = list(cv("TWO_FACTOR_ENABLED_METHODS"))802 if "email" in choices:803 # backwards compat804 choices.append("mail")805 if not cv("TWO_FACTOR_REQUIRED"):806 choices.append("disable")807 if "setup" not in self.data or self.data["setup"] not in choices:808 self.setup.errors.append(get_message("TWO_FACTOR_METHOD_NOT_AVAILABLE")[0])809 return False810 if self.setup.data == "sms":811 msg = _security._phone_util.validate_phone_number(self.phone.data)812 if msg:813 self.phone.errors.append(msg)814 return False815 816 return True817 818 819class TwoFactorVerifyCodeForm(Form, CodeFormMixin):820 """The Two-factor token validation form"""821 822 submit = SubmitField(get_form_field_label("submitcode"))823 824 def __init__(self, *args: t.Any, **kwargs: t.Any):825 super().__init__(*args, **kwargs)826 # These are set by view.827 self.window: int = 0828 self.primary_method: str = ""829 self.tf_totp_secret: str = ""830 self.user: User | None = None # set by view831 832 def validate(self, **kwargs: t.Any) -> bool:833 if not super().validate(**kwargs): # pragma: no cover834 return False835 if (836 self.primary_method == "google_authenticator"837 or self.primary_method == "authenticator"838 ):839 self.window = cv("TWO_FACTOR_AUTHENTICATOR_VALIDITY")840 elif self.primary_method == "email" or self.primary_method == "mail":841 self.window = cv("TWO_FACTOR_MAIL_VALIDITY")842 elif self.primary_method == "sms":843 self.window = cv("TWO_FACTOR_SMS_VALIDITY")844 else:845 return False846 847 # verify entered code with user's totp secret848 assert self.user is not None849 if not _security._totp_factory.verify_totp(850 token=self.code.data,851 totp_secret=self.tf_totp_secret,852 user=self.user,853 window=self.window,854 ):855 self.code.errors.append(get_message("TWO_FACTOR_INVALID_TOKEN")[0])856 return False857 858 return True859 860 861class TwoFactorRescueForm(Form):862 """The Two-factor Rescue validation form"""863 864 # rescue options - additional options are generated in set_rescue_options()865 help_setup = RadioField(866 get_form_field_xlate(_("Trouble Accessing Your Account?/Lost Mobile Device?")),867 choices=[868 ("help", get_form_field_xlate(_("Contact Administrator"))),869 ],870 )871 submit = SubmitField(get_form_field_label("submit"))872 873 874class DummyForm(Form):875 """A dummy form for json responses"""876 877 def __init__(self, *args: t.Any, **kwargs: t.Any):878 super().__init__(*args, **kwargs)879 self.user: User | None = kwargs.get("user", None)880 881 882def build_form_from_request(form_name: str, **kwargs: dict[str, t.Any]) -> Form:883 # helper function for views884 form_data = None885 if request.content_length:886 form_data = MultiDict(request.get_json()) if request.is_json else request.form887 return build_form(888 form_name, formdata=form_data, meta=suppress_form_csrf(), **kwargs889 )890 891 892def build_form(form_name, **kwargs):893 # helper function for views894 kwargs.setdefault("formdata", None)895 return _security.forms[form_name].instantiator(896 form_name,897 _security.forms[form_name].cls,898 **kwargs,899 )900 