Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
forms.py900 linesDownload Raw Back to flask_security
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 
codekingpro/portable-devtools · Team Ai