codekingpro/portable-devtools
114k
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 