Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
core.py1965 linesDownload Raw Back to flask_security
1"""2    flask_security.core3    ~~~~~~~~~~~~~~~~~~~4 5    Flask-Security core module6 7    :copyright: (c) 2012 by Matt Wright.8    :copyright: (c) 2017 by CERN.9    :copyright: (c) 2017 by ETH Zurich, Swiss Data Science Center.10    :copyright: (c) 2019-2024 by J. Christopher Wagner (jwag).11    :license: MIT, see LICENSE for more details.12"""13 14from __future__ import annotations15 16from datetime import datetime, timedelta17from dataclasses import dataclass18import importlib19import typing as t20import warnings21 22from flask import current_app, g23from flask_login import AnonymousUserMixin, LoginManager24from flask_login import UserMixin as BaseUserMixin25from flask_login import current_user26from flask_principal import Identity, Principal, RoleNeed, UserNeed, identity_loaded27from itsdangerous import URLSafeTimedSerializer28from passlib.context import CryptContext29from werkzeug.datastructures import ImmutableList30from werkzeug.local import LocalProxy31 32from .babel import FsDomain33from .decorators import (34    default_reauthn_handler,35    default_unauthn_handler,36    default_unauthz_handler,37)38from .forms import (39    ChangePasswordForm,40    ConfirmRegisterForm,41    ForgotPasswordForm,42    Form,43    LoginForm,44    PasswordlessLoginForm,45    RegisterForm,46    RegisterFormMixin,47    ResetPasswordForm,48    SendConfirmationForm,49    TwoFactorVerifyCodeForm,50    TwoFactorSetupForm,51    TwoFactorRescueForm,52    VerifyForm,53    get_register_username_field,54    login_username_field,55)56from .json import setup_json57from .mail_util import MailUtil58from .password_util import PasswordUtil59from .phone_util import PhoneUtil60from .oauth_glue import OAuthGlue61from .proxies import _security62from .recovery_codes import (63    MfRecoveryForm,64    MfRecoveryCodesForm,65    MfRecoveryCodesUtil,66)67from .tf_plugin import TfPlugin, TwoFactorSelectForm68from .twofactor import tf_send_security_token69from .unified_signin import (70    UnifiedSigninForm,71    UnifiedSigninSetupForm,72    UnifiedSigninSetupValidateForm,73    UnifiedVerifyForm,74    us_send_security_token,75)76from .webauthn import (77    WebAuthnDeleteForm,78    WebAuthnRegisterForm,79    WebAuthnRegisterResponseForm,80    WebAuthnSigninForm,81    WebAuthnSigninResponseForm,82    WebAuthnVerifyForm,83)84from .webauthn_util import WebauthnUtil85from .username_util import UsernameUtil86from .totp import Totp87from .utils import _88from .utils import config_value as cv89from .utils import (90    FsPermNeed,91    csrf_cookie_handler,92    default_render_template,93    default_want_json,94    get_identity_attribute,95    get_identity_attributes,96    get_message,97    get_request_attr,98    is_user_authenticated,99    naive_utcnow,100    parse_auth_token,101    set_request_attr,102    uia_email_mapper,103    uia_username_mapper,104    url_for_security,105    verify_and_update_password,106)107from .views import create_blueprint, default_render_json108 109if t.TYPE_CHECKING:  # pragma: no cover110    import flask111    from flask import Request112    from flask.typing import ResponseValue113    import flask_login.mixins114    from authlib.integrations.flask_client import OAuth115    from .datastore import Role, User, UserDatastore116 117 118# List of authentication mechanisms supported.119AUTHN_MECHANISMS = ("basic", "session", "token")120 121 122#: Default Flask-Security configuration123_default_config: dict[str, t.Any] = {124    "ANONYMOUS_USER_DISABLED": False,125    "BLUEPRINT_NAME": "security",126    "CLI_ROLES_NAME": "roles",127    "CLI_USERS_NAME": "users",128    "URL_PREFIX": None,129    "STATIC_FOLDER": "static",130    "STATIC_FOLDER_URL": "/fs-static",131    "SUBDOMAIN": None,132    "FLASH_MESSAGES": True,133    "RETURN_GENERIC_RESPONSES": False,134    "I18N_DOMAIN": "flask_security",135    "I18N_DIRNAME": "builtin",136    "EMAIL_VALIDATOR_ARGS": None,137    "PASSWORD_HASH": "bcrypt",138    "PASSWORD_SALT": None,139    "PASSWORD_SINGLE_HASH": {140        "django_argon2",141        "django_bcrypt_sha256",142        "django_pbkdf2_sha256",143        "django_pbkdf2_sha1",144        "django_bcrypt",145        "django_salted_md5",146        "django_salted_sha1",147        "django_des_crypt",148        "plaintext",149    },150    "PASSWORD_SCHEMES": [151        "bcrypt",152        "argon2",153        "des_crypt",154        "pbkdf2_sha256",155        "pbkdf2_sha512",156        "sha256_crypt",157        "sha512_crypt",158        # And always last one...159        "plaintext",160    ],161    "PASSWORD_HASH_OPTIONS": {},  # Deprecated at passlib 1.7162    "PASSWORD_HASH_PASSLIB_OPTIONS": {163        "argon2__rounds": 10  # 1.7.1 default is 2.164    },  # >= 1.7.1 method to pass options.165    "PASSWORD_LENGTH_MIN": 8,166    "PASSWORD_COMPLEXITY_CHECKER": None,167    "PASSWORD_CHECK_BREACHED": False,168    "PASSWORD_BREACHED_COUNT": 1,169    "PASSWORD_NORMALIZE_FORM": "NFKD",170    "PASSWORD_REQUIRED": True,171    "DEPRECATED_PASSWORD_SCHEMES": ["auto"],172    "LOGIN_URL": "/login",173    "LOGOUT_URL": "/logout",174    "REGISTER_URL": "/register",175    "RESET_URL": "/reset",176    "CHANGE_URL": "/change",177    "CONFIRM_URL": "/confirm",178    "VERIFY_URL": "/verify",179    "TWO_FACTOR_SETUP_URL": "/tf-setup",180    "TWO_FACTOR_TOKEN_VALIDATION_URL": "/tf-validate",181    "TWO_FACTOR_RESCUE_URL": "/tf-rescue",182    "TWO_FACTOR_SELECT_URL": "/tf-select",183    "TWO_FACTOR_POST_SETUP_VIEW": ".two_factor_setup",  # endpoint or URL184    "TWO_FACTOR_ERROR_VIEW": ".login",185    "LOGOUT_METHODS": ["GET", "POST"],186    "POST_LOGIN_VIEW": "/",187    "POST_LOGOUT_VIEW": "/",188    "LOGIN_ERROR_VIEW": None,  # spa189    "POST_OAUTH_LOGIN_VIEW": None,  # spa190    "CONFIRM_ERROR_VIEW": None,  # spa191    "POST_CONFIRM_VIEW": None,  # spa192    "RESET_VIEW": None,  # spa193    "RESET_ERROR_VIEW": None,  # spa194    "POST_RESET_VIEW": None,195    "POST_CHANGE_VIEW": None,196    "POST_VERIFY_VIEW": None,197    "POST_REGISTER_VIEW": None,198    "UNAUTHORIZED_VIEW": None,199    "REQUIRES_CONFIRMATION_ERROR_VIEW": None,200    "REDIRECT_HOST": None,201    "REDIRECT_BEHAVIOR": None,202    "REDIRECT_ALLOW_SUBDOMAINS": False,203    "FORGOT_PASSWORD_TEMPLATE": "security/forgot_password.html",204    "LOGIN_USER_TEMPLATE": "security/login_user.html",205    "REGISTER_USER_TEMPLATE": "security/register_user.html",206    "RESET_PASSWORD_TEMPLATE": "security/reset_password.html",207    "CHANGE_PASSWORD_TEMPLATE": "security/change_password.html",208    "SEND_CONFIRMATION_TEMPLATE": "security/send_confirmation.html",209    "SEND_LOGIN_TEMPLATE": "security/send_login.html",210    "VERIFY_TEMPLATE": "security/verify.html",211    "TWO_FACTOR_VERIFY_CODE_TEMPLATE": "security/two_factor_verify_code.html",212    "TWO_FACTOR_SETUP_TEMPLATE": "security/two_factor_setup.html",213    "TWO_FACTOR_SELECT_TEMPLATE": "security/two_factor_select.html",214    "CONFIRMABLE": False,215    "REGISTERABLE": False,216    "RECOVERABLE": False,217    "TRACKABLE": False,218    "PASSWORDLESS": False,219    "CHANGEABLE": False,220    "TWO_FACTOR": False,221    "SEND_REGISTER_EMAIL": True,222    "SEND_PASSWORD_CHANGE_EMAIL": True,223    "SEND_PASSWORD_RESET_EMAIL": True,224    "SEND_PASSWORD_RESET_NOTICE_EMAIL": True,225    "LOGIN_WITHIN": "1 days",226    "TWO_FACTOR_AUTHENTICATOR_VALIDITY": 120,227    "TWO_FACTOR_MAIL_VALIDITY": 300,228    "TWO_FACTOR_SMS_VALIDITY": 120,229    "TWO_FACTOR_ALWAYS_VALIDATE": True,230    "TWO_FACTOR_LOGIN_VALIDITY": "30 days",231    "TWO_FACTOR_VALIDITY_SALT": "tf-validity-salt",232    "TWO_FACTOR_VALIDITY_COOKIE": {233        "httponly": True,234        "secure": False,235        "samesite": "Strict",236    },237    "TWO_FACTOR_RESCUE_EMAIL": True,238    "MULTI_FACTOR_RECOVERY_CODES": False,239    "MULTI_FACTOR_RECOVERY_CODES_N": 5,240    "MULTI_FACTOR_RECOVERY_CODES_URL": "/mf-recovery-codes",241    "MULTI_FACTOR_RECOVERY_CODES_TEMPLATE": "security/mf_recovery_codes.html",242    "MULTI_FACTOR_RECOVERY_URL": "/mf-recovery",243    "MULTI_FACTOR_RECOVERY_TEMPLATE": "security/mf_recovery.html",244    "MULTI_FACTOR_RECOVERY_CODES_KEYS": None,245    "MULTI_FACTOR_RECOVERY_CODE_TTL": None,246    "OAUTH_ENABLE": False,247    "OAUTH_BUILTIN_PROVIDERS": ["github", "google"],248    "OAUTH_START_URL": "/login/oauthstart",249    "OAUTH_RESPONSE_URL": "/login/oauthresponse",250    "CONFIRM_EMAIL_WITHIN": "5 days",251    "RESET_PASSWORD_WITHIN": "1 days",252    "LOGIN_WITHOUT_CONFIRMATION": False,253    "AUTO_LOGIN_AFTER_CONFIRM": False,254    "AUTO_LOGIN_AFTER_RESET": False,255    "EMAIL_SENDER": LocalProxy(256        lambda: current_app.config.get("MAIL_DEFAULT_SENDER", "no-reply@localhost")257    ),258    "TWO_FACTOR_RESCUE_MAIL": "no-reply@localhost",259    "TOKEN_AUTHENTICATION_KEY": "auth_token",260    "TOKEN_AUTHENTICATION_HEADER": "Authentication-Token",261    "TOKEN_MAX_AGE": None,262    "TOKEN_EXPIRE_TIMESTAMP": lambda user: 0,263    "CONFIRM_SALT": "confirm-salt",264    "RESET_SALT": "reset-salt",265    "LOGIN_SALT": "login-salt",266    "CHANGE_SALT": "change-salt",267    "REMEMBER_SALT": "remember-salt",268    "DEFAULT_REMEMBER_ME": False,269    "DEFAULT_HTTP_AUTH_REALM": _("Login Required"),270    "EMAIL_SUBJECT_REGISTER": _("Welcome"),271    "EMAIL_SUBJECT_CONFIRM": _("Please confirm your email"),272    "EMAIL_SUBJECT_PASSWORDLESS": _("Login instructions"),273    "EMAIL_SUBJECT_PASSWORD_NOTICE": _("Your password has been reset"),274    "EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE": _("Your password has been changed"),275    "EMAIL_SUBJECT_PASSWORD_RESET": _("Password reset instructions"),276    "EMAIL_PLAINTEXT": True,277    "EMAIL_HTML": True,278    "EMAIL_SUBJECT_TWO_FACTOR": _("Two-factor Login"),279    "EMAIL_SUBJECT_TWO_FACTOR_RESCUE": _("Two-factor Rescue"),280    "USER_IDENTITY_ATTRIBUTES": [281        {"email": {"mapper": uia_email_mapper, "case_insensitive": True}}282    ],283    "PHONE_REGION_DEFAULT": "US",284    "FRESHNESS": timedelta(hours=24),285    "FRESHNESS_GRACE_PERIOD": timedelta(hours=1),286    "API_ENABLED_METHODS": ["session", "token"],287    "HASHING_SCHEMES": ["sha256_crypt", "hex_md5"],288    "DEPRECATED_HASHING_SCHEMES": ["hex_md5"],289    "DATETIME_FACTORY": naive_utcnow,290    "TOTP_SECRETS": None,291    "TOTP_ISSUER": None,292    "SMS_SERVICE": "Dummy",293    "SMS_SERVICE_CONFIG": {294        "ACCOUNT_SID": None,295        "AUTH_TOKEN": None,296        "PHONE_NUMBER": None,297    },298    "TWO_FACTOR_REQUIRED": False,299    "TWO_FACTOR_SECRET": None,  # Deprecated - use TOTP_SECRETS300    "TWO_FACTOR_ENABLED_METHODS": ["email", "authenticator", "sms"],301    "TWO_FACTOR_URI_SERVICE_NAME": "service_name",  # Deprecated - use TOTP_ISSUER302    "TWO_FACTOR_SMS_SERVICE": "Dummy",  # Deprecated - use SMS_SERVICE303    "TWO_FACTOR_SMS_SERVICE_CONFIG": {  # Deprecated - use SMS_SERVICE_CONFIG304        "ACCOUNT_SID": None,305        "AUTH_TOKEN": None,306        "PHONE_NUMBER": None,307    },308    "TWO_FACTOR_IMPLEMENTATIONS": {309        "code": "flask_security.twofactor.CodeTfPlugin",310        "webauthn": "flask_security.webauthn.WebAuthnTfPlugin",311    },312    "UNIFIED_SIGNIN": False,313    "US_SETUP_SALT": "us-setup-salt",314    "US_SIGNIN_URL": "/us-signin",315    "US_SIGNIN_SEND_CODE_URL": "/us-signin/send-code",316    "US_SETUP_URL": "/us-setup",317    "US_VERIFY_URL": "/us-verify",318    "US_VERIFY_SEND_CODE_URL": "/us-verify/send-code",319    "US_VERIFY_LINK_URL": "/us-verify-link",320    "US_POST_SETUP_VIEW": ".us_setup",  # endpoint or URL321    "US_SIGNIN_TEMPLATE": "security/us_signin.html",322    "US_SETUP_TEMPLATE": "security/us_setup.html",323    "US_VERIFY_TEMPLATE": "security/us_verify.html",324    "US_ENABLED_METHODS": ["password", "email", "authenticator", "sms"],325    "US_MFA_REQUIRED": ["password", "email"],326    "US_TOKEN_VALIDITY": 120,327    "US_EMAIL_SUBJECT": _("Verification Code"),328    "US_SETUP_WITHIN": "30 minutes",329    "US_SIGNIN_REPLACES_LOGIN": False,330    "CSRF_PROTECT_MECHANISMS": AUTHN_MECHANISMS,331    "CSRF_IGNORE_UNAUTH_ENDPOINTS": False,332    "CSRF_COOKIE_NAME": None,333    "CSRF_COOKIE": {334        "samesite": "Strict",335        "httponly": False,336        "secure": False,337    },338    "CSRF_HEADER": "X-XSRF-Token",339    "CSRF_COOKIE_REFRESH_EACH_REQUEST": False,340    "BACKWARDS_COMPAT_UNAUTHN": False,341    "BACKWARDS_COMPAT_AUTH_TOKEN": False,342    "JOIN_USER_ROLES": True,343    "USERNAME_ENABLE": False,344    "USERNAME_REQUIRED": False,345    "USERNAME_MIN_LENGTH": 4,346    "USERNAME_MAX_LENGTH": 32,347    "USERNAME_NORMALIZE_FORM": "NFKD",348    "WEBAUTHN": False,349    "WAN_CHALLENGE_BYTES": None,  # uses system default350    "WAN_POST_REGISTER_VIEW": ".wan_register",  # endpoint or URL351    "WAN_RP_NAME": "My Flask App",352    "WAN_SALT": "wan-salt",353    "WAN_REGISTER_TIMEOUT": 60000,  # milliseconds354    "WAN_REGISTER_TEMPLATE": "security/wan_register.html",355    "WAN_REGISTER_URL": "/wan-register",356    "WAN_REGISTER_WITHIN": "30 minutes",357    "WAN_SIGNIN_TIMEOUT": 60000,  # milliseconds358    "WAN_SIGNIN_TEMPLATE": "security/wan_signin.html",359    "WAN_SIGNIN_URL": "/wan-signin",360    "WAN_SIGNIN_WITHIN": "1 minutes",361    "WAN_DELETE_URL": "/wan-delete",362    "WAN_VERIFY_URL": "/wan-verify",363    "WAN_VERIFY_TEMPLATE": "security/wan_verify.html",364    "WAN_ALLOW_AS_FIRST_FACTOR": True,365    "WAN_ALLOW_AS_MULTI_FACTOR": True,366    "WAN_ALLOW_USER_HINTS": True,367    "WAN_ALLOW_AS_VERIFY": ["first", "secondary"],368    "ZXCVBN_MINIMUM_SCORE": 3,369}370 371#: Default Flask-Security messages372_default_messages = {373    "API_ERROR": (_("Input not appropriate for requested API"), "error"),374    "GENERIC_AUTHN_FAILED": (375        _("Authentication failed - identity or password/passcode invalid"),376        "error",377    ),378    "GENERIC_RECOVERY": (379        _(380            "If that email address is in our system, "381            "you will receive an email describing how to reset your password."382        ),383        "info",384    ),385    "GENERIC_US_SIGNIN": (386        _("If that identity is in our system, you were sent a code."),387        "info",388    ),389    "UNAUTHORIZED": (_("You do not have permission to view this resource."), "error"),390    "UNAUTHENTICATED": (391        _("You must sign in to view this resource."),392        "error",393    ),394    "REAUTHENTICATION_REQUIRED": (395        _("You must re-authenticate to access this endpoint"),396        "error",397    ),398    "CONFIRM_REGISTRATION": (399        _(400            "Thank you. To confirm your email address %(email)s,"401            " please click on the link"402            " in the email we have just sent to you."403        ),404        "success",405    ),406    "EMAIL_CONFIRMED": (_("Thank you. Your email has been confirmed."), "success"),407    "ALREADY_CONFIRMED": (_("Your email has already been confirmed."), "info"),408    "INVALID_CONFIRMATION_TOKEN": (_("Invalid confirmation token."), "error"),409    "EMAIL_ALREADY_ASSOCIATED": (410        _("%(email)s is already associated with an account."),411        "error",412    ),413    "IDENTITY_ALREADY_ASSOCIATED": (414        _(415            "Identity attribute '%(attr)s' with value '%(value)s' is already"416            " associated with an account."417        ),418        "error",419    ),420    "IDENTITY_NOT_REGISTERED": (421        _("Identity %(id)s not registered"),422        "error",423    ),424    "OAUTH_HANDSHAKE_ERROR": (425        _(426            "An error occurred while communicating with the Oauth provider:"427            " (%(exerror)s - %(exdesc)s). "428            "Please try again."429        ),430        "error",431    ),432    "PASSWORD_MISMATCH": (_("Password does not match"), "error"),433    "RETYPE_PASSWORD_MISMATCH": (_("Passwords do not match"), "error"),434    "INVALID_REDIRECT": (_("Redirections outside the domain are forbidden"), "error"),435    "INVALID_RECOVERY_CODE": (_("Recovery code invalid"), "error"),436    "NO_RECOVERY_CODES_SETUP": (_("No recovery codes generated yet"), "info"),437    "PASSWORD_RESET_REQUEST": (438        _("Instructions to reset your password have been sent to %(email)s."),439        "info",440    ),441    "PASSWORD_RESET_EXPIRED": (442        _("You did not reset your password within %(within)s. "),443        "error",444    ),445    "INVALID_RESET_PASSWORD_TOKEN": (_("Invalid reset password token."), "error"),446    "CONFIRMATION_REQUIRED": (_("Email requires confirmation."), "error"),447    "CONFIRMATION_REQUEST": (448        _("Confirmation instructions have been sent to %(email)s."),449        "info",450    ),451    "CONFIRMATION_EXPIRED": (452        _("You did not confirm your email within %(within)s. "),453        "error",454    ),455    "LOGIN_EXPIRED": (456        _(457            "You did not login within %(within)s. New instructions to login "458            "have been sent to %(email)s."459        ),460        "error",461    ),462    "LOGIN_EMAIL_SENT": (463        _("Instructions to login have been sent to %(email)s."),464        "success",465    ),466    "INVALID_LOGIN_TOKEN": (_("Invalid login token."), "error"),467    "DISABLED_ACCOUNT": (_("Account is disabled."), "error"),468    "EMAIL_NOT_PROVIDED": (_("Email not provided"), "error"),469    "INVALID_EMAIL_ADDRESS": (_("Invalid email address"), "error"),470    "INVALID_CODE": (_("Invalid code"), "error"),471    "PASSWORD_NOT_PROVIDED": (_("Password not provided"), "error"),472    "PASSWORD_INVALID_LENGTH": (473        _("Password must be at least %(length)s characters"),474        "error",475    ),476    "PASSWORD_TOO_SIMPLE": (_("Password not complex enough"), "error"),477    "PASSWORD_BREACHED": (_("Password on breached list"), "error"),478    "PASSWORD_BREACHED_SITE_ERROR": (479        _("Failed to contact breached passwords site"),480        "error",481    ),482    "PHONE_INVALID": (_("Phone number not valid e.g. missing country code"), "error"),483    "USER_DOES_NOT_EXIST": (_("Specified user does not exist"), "error"),484    "INVALID_PASSWORD": (_("Invalid password"), "error"),485    "INVALID_PASSWORD_CODE": (_("Password or code submitted is not valid"), "error"),486    "PASSWORDLESS_LOGIN_SUCCESSFUL": (_("You have successfully logged in."), "success"),487    "FORGOT_PASSWORD": (_("Forgot password?"), "info"),488    "PASSWORD_RESET": (489        _(490            "You successfully reset your password and you have been logged in "491            "automatically."492        ),493        "success",494    ),495    "PASSWORD_RESET_NO_LOGIN": (496        _(497            "You successfully reset your password."498            " Please authenticate using your new password."499        ),500        "success",501    ),502    "PASSWORD_IS_THE_SAME": (503        _("Your new password must be different than your previous password."),504        "error",505    ),506    "PASSWORD_CHANGE": (_("You successfully changed your password."), "success"),507    "LOGIN": (_("Please log in to access this page."), "info"),508    "REFRESH": (_("Please reauthenticate to access this page."), "info"),509    "REAUTHENTICATION_SUCCESSFUL": (_("Reauthentication successful"), "info"),510    "ANONYMOUS_USER_REQUIRED": (511        _("You can only access this endpoint when not logged in."),512        "error",513    ),514    "CODE_HAS_BEEN_SENT": (_("Code has been sent."), "info"),515    "FAILED_TO_SEND_CODE": (_("Failed to send code. Please try again later"), "error"),516    "TWO_FACTOR_INVALID_TOKEN": (_("Invalid code"), "error"),517    "TWO_FACTOR_LOGIN_SUCCESSFUL": (_("Your code has been confirmed"), "success"),518    "TWO_FACTOR_CHANGE_METHOD_SUCCESSFUL": (519        _("You successfully changed your two-factor method."),520        "success",521    ),522    "TWO_FACTOR_PERMISSION_DENIED": (523        _("You currently do not have permissions to access this page"),524        "error",525    ),526    "TWO_FACTOR_METHOD_NOT_AVAILABLE": (_("Marked method is not valid"), "error"),527    "TWO_FACTOR_DISABLED": (528        _("You successfully disabled two factor authorization."),529        "success",530    ),531    "US_CURRENT_METHODS": (532        _("Currently active sign in options: %(method_list)s."),533        "info",534    ),535    "US_METHOD_NOT_AVAILABLE": (_("Requested method is not valid"), "error"),536    "US_SETUP_EXPIRED": (537        _("Setup must be completed within %(within)s. Please start over."),538        "error",539    ),540    "US_SETUP_SUCCESSFUL": (_("Unified sign in setup successful"), "info"),541    "US_SPECIFY_IDENTITY": (_("You must specify a valid identity to sign in"), "error"),542    "USE_CODE": (_("Use this code to sign in: %(code)s."), "info"),543    "USERNAME_INVALID_LENGTH": (544        _(545            "Username must be at least %(min)d characters and less than"546            " %(max)d characters"547        ),548        "error",549    ),550    "USERNAME_ILLEGAL_CHARACTERS": (551        _("Username contains illegal characters"),552        "error",553    ),554    "USERNAME_DISALLOWED_CHARACTERS": (555        _("Username can contain only letters and numbers"),556        "error",557    ),558    "USERNAME_NOT_PROVIDED": (_("Username not provided"), "error"),559    "USERNAME_ALREADY_ASSOCIATED": (560        _("%(username)s is already associated with an account."),561        "error",562    ),563    "WEBAUTHN_EXPIRED": (564        _("WebAuthn operation must be completed within %(within)s. Please start over."),565        "error",566    ),567    "WEBAUTHN_NAME_REQUIRED": (568        _("Nickname for new credential is required."),569        "error",570    ),571    "WEBAUTHN_NAME_INUSE": (572        _("%(name)s is already associated with a credential."),573        "error",574    ),575    "WEBAUTHN_NAME_NOT_FOUND": (576        _("%(name)s not registered with current user."),577        "error",578    ),579    "WEBAUTHN_CREDENTIAL_DELETED": (580        _("Successfully deleted WebAuthn credential with name: %(name)s"),581        "info",582    ),583    "WEBAUTHN_REGISTER_SUCCESSFUL": (584        _("Successfully added WebAuthn credential with name: %(name)s"),585        "info",586    ),587    "WEBAUTHN_CREDENTIAL_ID_INUSE": (588        _("WebAuthn credential id already registered."),589        "error",590    ),591    "WEBAUTHN_UNKNOWN_CREDENTIAL_ID": (592        _("Unregistered WebAuthn credential id."),593        "error",594    ),595    "WEBAUTHN_ORPHAN_CREDENTIAL_ID": (596        _("WebAuthn credential doesn't belong to any user."),597        "error",598    ),599    "WEBAUTHN_NO_VERIFY": (600        _("Could not verify WebAuthn credential: %(cause)s."),601        "error",602    ),603    "WEBAUTHN_CREDENTIAL_WRONG_USAGE": (604        _("Credential not registered for this use (first or secondary)"),605        "error",606    ),607    "WEBAUTHN_MISMATCH_USER_HANDLE": (608        _("Credential user handle didn't match"),609        "error",610    ),611}612 613 614def _default_form_instantiator(615    name: str, cls: t.Type[Form], *args: t.Any, **kwargs: dict[str, t.Any]616) -> Form:617    return cls(*args, **kwargs)618 619 620@dataclass621class FormInfo:622    """623    Each view form has a name - assigned by Flask-Security.624    As part of every request, the form is instantiated using (usually) request.form or625    request.json.626    The default instantiator simply uses the class constructor - however627    applications can provide their OWN instantiator which can do pretty much anything628    as long as it returns an instantiated form. The 'cls' argument is optional since629    the instantiator COULD be form specific.630 631    The instantiator callable will always be called from a flask request context632    and receive the following arguments::633 634        (name, form_cls_name (optional), **kwargs)635 636    kwargs will always have `formdata` and often will have `meta`. All kwargs637    must be passed to the underlying form constructor.638 639    See :py:meth:`flask_security.Security.set_form_info`640 641    .. versionadded:: 5.1.0642    """643 644    instantiator: t.Callable[..., Form] = _default_form_instantiator645    cls: t.Type[Form] | None = None646 647 648def _user_loader(user_id):649    """Load based on fs_uniquifier (alternative_id)."""650    user = _security.datastore.find_user(fs_uniquifier=str(user_id))651    if user and user.active:652        set_request_attr("fs_authn_via", "session")653        return user654    return None655 656 657def _request_loader(request):658    # Short-circuit if we have already been called and verified.659    # This can happen since Flask-Login will call us (if no session) and our own660    # decorator @auth_token_required can call us.661    # N.B. we don't call current_user here since that in fact might try and LOAD662    # a user - which would call us again.663    if get_request_attr("fs_authn_via") == "token":664        return g._login_user665 666    header_key = cv("TOKEN_AUTHENTICATION_HEADER")667    args_key = cv("TOKEN_AUTHENTICATION_KEY")668    header_token = request.headers.get(header_key, None)669    token = request.args.get(args_key, header_token)670    if request.is_json:671        data = request.get_json(silent=True) or {}672        if isinstance(data, dict):673            token = data.get(args_key, token)674 675    try:676        tdata = parse_auth_token(token)677        if hasattr(_security.datastore.user_model, "fs_token_uniquifier"):678            user = _security.datastore.find_user(fs_token_uniquifier=tdata["uid"])679        else:680            user = _security.datastore.find_user(fs_uniquifier=tdata["uid"])681    except Exception:682        return None683 684    if user and user.active and user.verify_auth_token(tdata):685        set_request_attr("fs_authn_via", "token")686        return user687 688    return None689 690 691def _identity_loader():692    # N.B. once AnonymousUser is gone - can just check current_user693    if current_user and hasattr(current_user, "fs_uniquifier"):694        return Identity(current_user.fs_uniquifier)695    return None696 697 698def _on_identity_loaded(sender, identity):699    if current_user and hasattr(current_user, "fs_uniquifier"):700        identity.provides.add(UserNeed(current_user.fs_uniquifier))701 702        for role in getattr(current_user, "roles", []):703            identity.provides.add(RoleNeed(role.name))704            for fsperm in role.get_permissions():705                identity.provides.add(FsPermNeed(fsperm))706 707    identity.user = current_user708 709 710def _get_login_manager(app, security):711    lm = LoginManager()712    # Flask-Login is likely going in the direction of removing AnonymousUser713    # however this might wreak havoc on applications that just assume that714    # current_user is always set.715    if cv("ANONYMOUS_USER_DISABLED", app=app):716        lm.anonymous_user = lambda: None717    else:718        lm.anonymous_user = AnonymousUser719 720    lm.user_loader(_user_loader)721    lm.request_loader(_request_loader)722    # Set Flask-Login handler so @login_required will have same behavior as723    # @auth_required724    lm.unauthorized_callback = security._unauthn_handler725 726    # Note: since we redirect unauthenticated requests to us - we no longer need to727    # mess with Flask-Login login_view, or message settings.728    # We also (5.4.0) stop doing anything with need_fresh_login - we support time-based729    # freshness.730 731    lm.init_app(app)732    return lm733 734 735def _get_principal(app):736    p = Principal(app, use_sessions=False)737    p.identity_loader(_identity_loader)738    return p739 740 741def _get_pwd_context(app: flask.Flask) -> CryptContext:742    pw_hash = cv("PASSWORD_HASH", app=app)743    schemes = cv("PASSWORD_SCHEMES", app=app)744    deprecated = cv("DEPRECATED_PASSWORD_SCHEMES", app=app)745    if pw_hash not in schemes:746        allowed = ", ".join(schemes[:-1]) + " and " + schemes[-1]747        raise ValueError(748            f"Invalid password hashing scheme {pw_hash}. Allowed values are {allowed}"749        )750    cc = CryptContext(751        schemes=schemes,752        default=pw_hash,753        deprecated=deprecated,754        **cv("PASSWORD_HASH_PASSLIB_OPTIONS", app=app),755    )756    return cc757 758 759def _get_hashing_context(app: flask.Flask) -> CryptContext:760    schemes = cv("HASHING_SCHEMES", app=app)761    deprecated = cv("DEPRECATED_HASHING_SCHEMES", app=app)762    return CryptContext(schemes=schemes, deprecated=deprecated)763 764 765def _get_serializer(app, name):766    secret_key = app.config.get("SECRET_KEY")767    salt = cv(f"{name.upper()}_SALT", app=app)768    return URLSafeTimedSerializer(secret_key=secret_key, salt=salt)769 770 771def _context_processor():772    return dict(773        url_for_security=url_for_security,774        security=_security,775        _fs_is_user_authenticated=is_user_authenticated,776    )777 778 779class RoleMixin:780    """Mixin for `Role` model definitions"""781 782    if t.TYPE_CHECKING:  # pragma: no cover783 784        def __init__(self) -> None:785            self.permissions: list[str] | None786 787    def __eq__(self, other):788        return self.name == other or self.name == getattr(other, "name", None)789 790    def __ne__(self, other):791        return not self.__eq__(other)792 793    def __hash__(self):794        return hash(self.name)795 796    def get_permissions(self) -> set:797        """798        Return set of permissions associated with role.799 800        .. versionadded:: 3.3.0801        """802        if hasattr(self, "permissions") and self.permissions:803            return set(self.permissions)804        return set()805 806 807class UserMixin(BaseUserMixin):808    """Mixin for `User` model definitions"""809 810    def get_id(self) -> str:811        """Returns the user identification attribute. 'Alternative-token' for812        Flask-Login. This is always ``fs_uniquifier``.813 814        .. versionadded:: 3.4.0815        """816        return str(self.fs_uniquifier)817 818    @property819    def is_active(self) -> bool:820        """Returns `True` if the user is active."""821        return self.active822 823    def get_auth_token(self) -> str | bytes:824        """Constructs the user's authentication token.825 826        :raises ValueError: If ``fs_token_uniquifier`` is part of model but not set.827 828        Optionally use a separate uniquifier so that changing password doesn't829        invalidate auth tokens.830 831        The returned value is securely signed using the ``remember_token_serializer``832 833        .. versionchanged:: 4.0.0834            If user model has ``fs_token_uniquifier`` - use that (raise ValueError835            if not set). Otherwise, fallback to using ``fs_uniquifier``.836        .. versionchanged:: 5.4.0837            New format - a dict with a version string. Add a token-based expiry838            option as well as a session id.839        """840 841        tdata: dict[str, t.Any] = dict(ver=str(5))842        if hasattr(self, "fs_token_uniquifier"):843            if not self.fs_token_uniquifier:844                raise ValueError()845            tdata["uid"] = str(self.fs_token_uniquifier)846        else:847            tdata["uid"] = str(self.fs_uniquifier)848        tdata["sid"] = 0  # session id849        tdata["exp"] = int(cv("TOKEN_EXPIRE_TIMESTAMP")(self))  # if >0 then shorter of850        # :data:SECURITY_MAX_AGE and this.851 852        # Let application add things853        self.augment_auth_token(tdata)854 855        # Serialize and sign856        return _security.remember_token_serializer.dumps(tdata)857 858    def augment_auth_token(self, tdata: dict[str, t.Any]) -> None:859        """Override this to add/modify parts of the auth token.860        Additions to the dict can be made and verified in verify_auth_token()861 862        .. versionadded:: 5.4.0863        """864        return865 866    def verify_auth_token(self, tdata: dict[str, t.Any]) -> bool:867        """868        Override this to perform additional verification of contents of auth token.869        Prior to this being called the token has been validated (via signing)870        and has not expired (either with MAX_AGE or specific 'exp' value).871 872        :param tdata: a dictionary just as in augment_auth_token()873        :return: True if auth token represented by tdata is valid, False otherwise.874 875        .. versionadded:: 3.3.0876 877        .. versionchanged:: 5.4.0878            Now receives a dictionary.879        """880        return True881 882    def has_role(self, role: str | Role) -> bool:883        """Returns `True` if the user identifies with the specified role.884 885        :param role: A role name or `Role` instance"""886        if isinstance(role, str):887            return role in (role.name for role in self.roles)888        else:889            return role in self.roles890 891    def has_permission(self, permission: str) -> bool:892        """893        Returns `True` if user has this permission (via a role it has).894 895        :param permission: permission string name896 897        .. versionadded:: 3.3.0898 899        """900        for role in self.roles:901            if permission in role.get_permissions():902                return True903        return False904 905    def get_security_payload(self) -> dict[str, t.Any]:906        """Serialize user object as response payload.907        Override this to return any/all of the user object in JSON responses.908        Return a dict.909        """910        return {}911 912    def get_redirect_qparams(913        self, existing: dict[str, t.Any] | None = None914    ) -> dict[str, t.Any]:915        """Return user info that will be added to redirect query params.916 917        :param existing: A dict that will be updated.918        :return: A dict whose keys will be query params and values will be query values.919 920        The returned dict will always have an 'identity' key/value.921        If the User Model contains 'email', an 'email' key/value will be added.922        All keys provided in 'existing' will also be merged in.923 924        .. versionadded:: 3.2.0925 926        .. versionchanged:: 4.0.0927            Add 'identity' using UserMixin.calc_username() - email is optional.928        """929        if not existing:930            existing = {}931        if hasattr(self, "email"):932            existing.update({"email": self.email})933        existing.update({"identity": self.calc_username()})934        return existing935 936    def verify_and_update_password(self, password: str) -> bool:937        """Returns ``True`` if the password is valid for the specified user.938 939        Additionally, the hashed password in the database is updated if the940        hashing algorithm happens to have changed.941 942        N.B. you MUST call DB commit if you are using a session-based datastore943        (such as SqlAlchemy) since the user instance might have been altered944        (i.e. ``app.security.datastore.commit()``).945        This is usually handled in the view.946 947        :param password: A plaintext password to verify948 949        .. versionadded:: 3.2.0950        """951        return verify_and_update_password(password, self)952 953    def calc_username(self) -> str:954        """Come up with the best 'username' based on how the app955        is configured (via :py:data:`SECURITY_USER_IDENTITY_ATTRIBUTES`).956        Returns the first non-null match (and converts to string).957        In theory this should NEVER be the empty string unless the user958        record isn't actually valid.959 960        .. versionadded:: 3.4.0961        """962        cusername = None963        for attr in get_identity_attributes():964            cusername = getattr(self, attr, None)965            if cusername is not None and len(str(cusername)) > 0:966                break967        return str(cusername) if cusername is not None else ""968 969    def us_send_security_token(self, method: str, **kwargs: t.Any) -> str | None:970        """Generate and send the security code for unified sign in.971 972        :param method: The method in which the code will be sent973        :param kwargs: Opaque parameters that are subject to change at any time974        :return: None if successful, error message if not.975 976        This is a wrapper around :meth:`us_send_security_token`977        that can be overridden to manage any errors.978 979        .. versionadded:: 3.4.0980        """981        try:982            us_send_security_token(self, method, **kwargs)983        except Exception:984            return get_message("FAILED_TO_SEND_CODE")[0]985        return None986 987    def tf_send_security_token(self, method: str, **kwargs: t.Any) -> str | None:988        """Generate and send the security code for two-factor.989 990        :param method: The method in which the code will be sent991        :param kwargs: Opaque parameters that are subject to change at any time992        :return: None if successful, error message if not.993 994        This is a wrapper around :meth:`tf_send_security_token`995        that can be overridden to manage any errors.996 997        .. versionadded:: 3.4.0998        """999        try:1000            tf_send_security_token(self, method, **kwargs)1001        except Exception:1002            return get_message("FAILED_TO_SEND_CODE")[0]1003        return None1004 1005 1006class WebAuthnMixin:1007    def get_user_mapping(self) -> dict[str, t.Any]:1008        """1009        Return the filter needed by find_user() to get the user1010        associated with this webauthn credential.1011        Note that this probably has to be overridden using mongoengine.1012 1013        .. versionadded:: 5.0.01014        """1015        return dict(id=self.user_id)  # type: ignore1016 1017 1018class AnonymousUser(AnonymousUserMixin):1019    """AnonymousUser definition"""1020 1021    def __init__(self):1022        self.roles = ImmutableList()1023 1024    def has_role(self, *args):1025        """Returns `False`"""1026        return False1027 1028 1029class Security:1030    """The :class:`Security` class initializes the Flask-Security extension.1031 1032    :param app: The application.1033    :param datastore: An instance of a user datastore.1034    :param register_blueprint: to register the Security blueprint or not.1035    :param login_form: set form for the login view1036    :param verify_form: set form for re-authentication due to freshness check1037    :param register_form: set form for the register view when1038            *SECURITY_CONFIRMABLE* is false1039    :param confirm_register_form: set form for the register view when1040            *SECURITY_CONFIRMABLE* is true1041    :param forgot_password_form: set form for the forgot password view1042    :param reset_password_form: set form for the reset password view1043    :param change_password_form: set form for the change password view1044    :param send_confirmation_form: set form for the send confirmation view1045    :param passwordless_login_form: set form for the passwordless login view1046    :param two_factor_setup_form: set form for the 2FA setup view1047    :param two_factor_verify_code_form: set form the the 2FA verify code view1048    :param two_factor_rescue_form: set form for the 2FA rescue view1049    :param two_factor_select_form: set form for selecting between active 2FA methods1050    :param mf_recovery_codes_form: set form for retrieving and setting recovery codes1051    :param mf_recovery_form: set form for multi factor recovery1052    :param us_signin_form: set form for the unified sign in view1053    :param us_setup_form: set form for the unified sign in setup view1054    :param us_setup_validate_form: set form for the unified sign in setup validate view1055    :param us_verify_form: set form for re-authenticating due to freshness check1056    :param wan_register_form: set form for registering a webauthn security key1057    :param wan_register_response_form: set form for registering a webauthn security key1058    :param wan_signin_form: set form for authenticating with a webauthn security key1059    :param wan_signin_response_form: set form for authenticating with a webauthn1060    :param wan_delete_form: set form for deleting a webauthn security key1061    :param wan_verify_form: set form for using a webauthn key to verify authenticity1062    :param mail_util_cls: Class to use for sending emails. Defaults to :class:`MailUtil`1063    :param password_util_cls: Class to use for password normalization/validation.1064     Defaults to :class:`PasswordUtil`1065    :param phone_util_cls: Class to use for phone number utilities.1066     Defaults to :class:`PhoneUtil`1067    :param render_template: function to use to render templates. The default is Flask's1068     render_template() function.1069    :param totp_cls: Class to use as TOTP factory. Defaults to :class:`Totp`1070    :param username_util_cls: Class to use for normalizing and validating usernames.1071        Defaults to :class:`UsernameUtil`1072    :param webauthn_util_cls: Class to use for customizing WebAuthn registration1073        and signin. Defaults to :class:`WebauthnUtil`1074    :param mf_recovery_codes_util_cls: Class for generating, checking, encrypting1075        and decrypting recovery codes. Defaults to :class:`MfRecoveryCodesUtil`1076    :param oauth: An instance of authlib.integrations.flask_client.OAuth. If not set,1077        Flask-Security will create one.1078 1079    .. tip::1080        Be sure that all your configuration values have been set PRIOR to1081        instantiating this class. Some configuration values are set as attributes1082        on the instance and therefore won't track any changes.1083 1084    .. versionadded:: 3.4.01085        ``verify_form`` added as part of freshness/re-authentication1086 1087    .. versionadded:: 3.4.01088        ``us_signin_form``, ``us_setup_form``, ``us_setup_validate_form``, and1089        ``us_verify_form`` added as part of the :ref:`unified-sign-in` feature.1090 1091    .. versionadded:: 3.4.01092        ``totp_cls`` added to enable applications to implement replay protection - see1093        :py:class:`Totp`.1094 1095    .. versionadded:: 3.4.01096        ``phone_util_cls`` added to allow different phone number1097         parsing implementations - see :py:class:`PhoneUtil`1098 1099    .. versionadded:: 4.0.01100        ``mail_util_cls`` added to isolate mailing handling.1101        ``password_util_cls`` added to encapsulate password validation/normalization.1102 1103    .. versionadded:: 4.1.01104        ``username_util_cls`` added to encapsulate username handling.1105 1106    .. versionadded:: 5.0.01107        ``wan_register_form``, ``wan_register_response_form``,1108         ``webauthn_signin_form``, ``wan_signin_response_form``,1109         ``webauthn_delete_form``, ``webauthn_verify_form``, ``tf_select_form``.1110    .. versionadded:: 5.0.01111        ``WebauthnUtil`` class.1112    .. versionadded:: 5.0.01113        Added support for multi-factor recovery codes ``mf_recovery_codes_form``,1114        ``mf_recovery_form``.1115    .. versionadded:: 5.1.01116        ``mf_recovery_codes_util_cls``, ``oauth``1117 1118    .. deprecated:: 4.0.01119        ``send_mail`` and ``send_mail_task``. Replaced with ``mail_util_cls``.1120        ``two_factor_verify_password_form`` removed.1121        ``password_validator`` removed in favor of the new ``password_util_cls``.1122    .. deprecated:: 5.0.01123        Passing in a LoginManager instance. Removed in 5.1.01124    .. deprecated:: 5.0.01125        json_encoder_cls is no longer honored since Flask 2.2 has deprecated it.1126    .. deprecated:: 5.3.11127        Passing in an anonymous_user class. Removed in 5.4.01128    """1129 1130    def __init__(1131        self,1132        app: flask.Flask | None = None,1133        datastore: UserDatastore | None = None,1134        register_blueprint: bool = True,1135        login_form: t.Type[LoginForm] = LoginForm,1136        verify_form: t.Type[VerifyForm] = VerifyForm,1137        confirm_register_form: t.Type[ConfirmRegisterForm] = ConfirmRegisterForm,1138        register_form: t.Type[RegisterForm] = RegisterForm,1139        forgot_password_form: t.Type[ForgotPasswordForm] = ForgotPasswordForm,1140        reset_password_form: t.Type[ResetPasswordForm] = ResetPasswordForm,1141        change_password_form: t.Type[ChangePasswordForm] = ChangePasswordForm,1142        send_confirmation_form: t.Type[SendConfirmationForm] = SendConfirmationForm,1143        passwordless_login_form: t.Type[PasswordlessLoginForm] = PasswordlessLoginForm,1144        two_factor_verify_code_form: t.Type[1145            TwoFactorVerifyCodeForm1146        ] = TwoFactorVerifyCodeForm,1147        two_factor_setup_form: t.Type[TwoFactorSetupForm] = TwoFactorSetupForm,1148        two_factor_rescue_form: t.Type[TwoFactorRescueForm] = TwoFactorRescueForm,1149        two_factor_select_form: t.Type[TwoFactorSelectForm] = TwoFactorSelectForm,1150        mf_recovery_codes_form: t.Type[MfRecoveryCodesForm] = MfRecoveryCodesForm,1151        mf_recovery_form: t.Type[MfRecoveryForm] = MfRecoveryForm,1152        us_signin_form: t.Type[UnifiedSigninForm] = UnifiedSigninForm,1153        us_setup_form: t.Type[UnifiedSigninSetupForm] = UnifiedSigninSetupForm,1154        us_setup_validate_form: t.Type[1155            UnifiedSigninSetupValidateForm1156        ] = UnifiedSigninSetupValidateForm,1157        us_verify_form: t.Type[UnifiedVerifyForm] = UnifiedVerifyForm,1158        wan_register_form: t.Type[WebAuthnRegisterForm] = WebAuthnRegisterForm,1159        wan_register_response_form: t.Type[1160            WebAuthnRegisterResponseForm1161        ] = WebAuthnRegisterResponseForm,1162        wan_signin_form: t.Type[WebAuthnSigninForm] = WebAuthnSigninForm,1163        wan_signin_response_form: t.Type[1164            WebAuthnSigninResponseForm1165        ] = WebAuthnSigninResponseForm,1166        wan_delete_form: t.Type[WebAuthnDeleteForm] = WebAuthnDeleteForm,1167        wan_verify_form: t.Type[WebAuthnVerifyForm] = WebAuthnVerifyForm,1168        mail_util_cls: t.Type[MailUtil] = MailUtil,1169        password_util_cls: t.Type[PasswordUtil] = PasswordUtil,1170        phone_util_cls: t.Type[PhoneUtil] = PhoneUtil,1171        render_template: t.Callable[..., str] = default_render_template,1172        totp_cls: t.Type[Totp] = Totp,1173        username_util_cls: t.Type[UsernameUtil] = UsernameUtil,1174        webauthn_util_cls: t.Type[WebauthnUtil] = WebauthnUtil,1175        mf_recovery_codes_util_cls: t.Type[MfRecoveryCodesUtil] = MfRecoveryCodesUtil,1176        oauth: OAuth | None = None,1177        **kwargs: t.Any,1178    ):1179        # to be nice and hopefully avoid backwards compat issues - we still accept1180        # kwargs - but we don't do anything with them. If caller sends in some -1181        # output a deprecation warning1182        if len(kwargs) > 0:1183            warnings.warn(1184                "kwargs passed to the constructor are now ignored",1185                DeprecationWarning,1186                stacklevel=2,1187            )1188        self.app = app1189        self._datastore = datastore1190        self._register_blueprint = register_blueprint1191        self.mail_util_cls = mail_util_cls1192        self.password_util_cls = password_util_cls1193        self.phone_util_cls = phone_util_cls1194        self.render_template = render_template1195        self.totp_cls = totp_cls1196        self.username_util_cls = username_util_cls1197        self.webauthn_util_cls = webauthn_util_cls1198        self.mf_recovery_codes_util_cls = mf_recovery_codes_util_cls1199        self._oauth = oauth1200 

Showing the first 1,200 of 1965 lines. Download the file for the rest.

codekingpro/portable-devtools · Team Ai