codekingpro/portable-devtools
114k
1"""2 flask_security.registerable3 ~~~~~~~~~~~~~~~~~~~~~~~~~~~4 5 Flask-Security registerable module6 7 :copyright: (c) 2012 by Matt Wright.8 :copyright: (c) 2019-2024 by J. Christopher Wagner (jwag).9 :license: MIT, see LICENSE for more details.10"""11 12from __future__ import annotations13 14import typing as t15 16from flask import current_app17 18from .confirmable import generate_confirmation_link19from .forms import form_errors_munge20from .proxies import _security, _datastore21from .signals import user_registered, user_not_registered22from .utils import (23 config_value as cv,24 do_flash,25 get_message,26 hash_password,27 send_mail,28 url_for_security,29)30 31if t.TYPE_CHECKING:32 from .forms import ConfirmRegisterForm33 34 35def register_user(registration_form):36 """37 Calls datastore to create user, triggers post-registration logic38 (e.g. sending confirmation link, sending registration mail)39 :param registration_form: form with user registration data40 :return: user instance41 """42 43 user_model_kwargs = registration_form.to_dict(only_user=True)44 45 # passwords are not always required -46 # with UNIFIED_SIGNIN and PASSWORD_REQUIRED=False47 if user_model_kwargs["password"]:48 user_model_kwargs["password"] = hash_password(user_model_kwargs["password"])49 user = _datastore.create_user(**user_model_kwargs)50 51 # if they didn't give a password - auto-setup email magic links (if UNIFIED SIGNIN)52 if not user_model_kwargs["password"] and cv("UNIFIED_SIGNIN"):53 _datastore.us_setup_email(user)54 55 confirmation_link, token = None, None56 if _security.confirmable:57 confirmation_link, token = generate_confirmation_link(user)58 do_flash(*get_message("CONFIRM_REGISTRATION", email=user.email))59 60 user_registered.send(61 current_app._get_current_object(),62 _async_wrapper=current_app.ensure_sync,63 user=user,64 confirm_token=token,65 confirmation_token=token,66 form_data=registration_form.to_dict(only_user=False),67 )68 69 if cv("SEND_REGISTER_EMAIL"):70 send_mail(71 cv("EMAIL_SUBJECT_REGISTER"),72 user.email,73 "welcome",74 user=user,75 confirmation_link=confirmation_link,76 confirmation_token=token,77 )78 79 return user80 81 82def register_existing(form: ConfirmRegisterForm) -> bool:83 """84 In the case of generic responses we want to mitigate any possible85 email/username enumeration.86 For an existing email we send an email to that address and tell them they87 are already registered (and provide their username if any).88 89 N.B. This (and forgot and confirm) could be used to DDOS an email by constantly90 issuing requests. One way to mitigate that is to use signals and add specific91 application code.92 93 Returning False means to return normal error messages.94 Returns True if the only 'error' is an existing email/user. In this case we95 simulate a normal registration and email the existing account to inform.96 97 """98 99 if not (100 cv("RETURN_GENERIC_RESPONSES")101 or form.existing_username_user102 or form.existing_email_user103 ): # pragma: no cover104 return False105 106 # There are 2 classes of error - an existing email/username and non-compliant107 # email/username/password. We want to give the user feedback on a non-compliant108 # input - but not give away whether the email/username is already taken.109 # Since in this case we have an 'existing' entry - we simply Null out those110 # errors.111 # This also means for JSON there is no way to tell if things worked or not.112 fields_to_squash: dict[str, dict[str, str]] = dict()113 if form.existing_email_user:114 fields_to_squash["email"] = dict()115 if hasattr(form, "username") and form.existing_username_user:116 fields_to_squash["username"] = dict()117 form_errors_munge(form, fields_to_squash)118 if form.errors:119 # some other illegal password/username - return an error120 return False121 122 # only errors were existing email/username123 hash_password("not-a-password") # reduce timing between successful and not.124 125 # Same as is done in register_user()126 if _security.confirmable:127 do_flash(*get_message("CONFIRM_REGISTRATION", email=form.email.data))128 129 # 2 cases:130 # 1) existing email (an already registered account) empty or same username131 # 2) new email with existing username (which corresponds to some OTHER account)132 133 if form.existing_email_user:134 user_not_registered.send(135 current_app._get_current_object(), # type: ignore136 _async_wrapper=current_app.ensure_sync,137 user=form.existing_email_user,138 existing_email=True,139 existing_username=form.existing_username_user is not None,140 form_data=form.to_dict(only_user=False),141 )142 # Send a nice email saying they are already registered - tell them their143 # existing username if they have one, and suggest how to reset password.144 recovery_link = None145 if _security.recoverable:146 recovery_link = url_for_security("forgot_password", _external=True)147 if cv("SEND_REGISTER_EMAIL"):148 send_mail(149 cv("EMAIL_SUBJECT_REGISTER"),150 form.existing_email_user.email,151 "welcome_existing",152 user=form.existing_email_user,153 recovery_link=recovery_link,154 )155 elif form.existing_username_user:156 # New email, already taken username.157 # Note that we send email to NEW email - so it is possible for a bad-actor158 # to enumerate usernames (slowly).159 user_not_registered.send(160 current_app._get_current_object(), # type: ignore[attr-defined]161 _async_wrapper=current_app.ensure_sync,162 user=None,163 existing_email=False,164 existing_username=True,165 form_data=form.to_dict(only_user=False),166 )167 if cv("SEND_REGISTER_EMAIL"):168 send_mail(169 cv("EMAIL_SUBJECT_REGISTER"),170 form.email.data,171 "welcome_existing_username",172 email=form.email.data,173 username=form.username.data if hasattr(form, "username") else None,174 )175 176 return True177 