diff --git a/flask_security/__init__.py b/flask_security/__init__.py index 1f72c84..e8364fc 100644 --- a/flask_security/__init__.py +++ b/flask_security/__init__.py @@ -20,6 +20,5 @@ from .decorators import auth_token_required, http_auth_required, \ from .forms import ForgotPasswordForm, LoginForm, RegisterForm, \ ResetPasswordForm, PasswordlessLoginForm from .signals import confirm_instructions_sent, password_reset, \ - password_reset_requested, reset_instructions_sent, user_confirmed, \ - user_registered + reset_password_instructions_sent, user_confirmed, user_registered from .utils import login_user, logout_user, url_for_security diff --git a/flask_security/confirmable.py b/flask_security/confirmable.py index 85b2e5d..cf738f0 100644 --- a/flask_security/confirmable.py +++ b/flask_security/confirmable.py @@ -26,12 +26,13 @@ _security = LocalProxy(lambda: app.extensions['security']) _datastore = LocalProxy(lambda: _security.datastore) -def send_confirmation_instructions(user, token): +def send_confirmation_instructions(user): """Sends the confirmation instructions email for the specified user. :param user: The user to send the instructions to :param token: The confirmation token """ + token = generate_confirmation_token(user) url = url_for_security('confirm_email', token=token) confirmation_link = request.url_root[:-1] + url @@ -43,6 +44,8 @@ def send_confirmation_instructions(user, token): confirm_instructions_sent.send(user, app=app._get_current_object()) + return token + def generate_confirmation_token(user): """Generates a unique confirmation token for the specified user. @@ -92,19 +95,3 @@ def confirm_by_token(token): except BadSignature: raise ConfirmationError(get_message('INVALID_CONFIRMATION_TOKEN')[0]) - - -def reset_confirmation_token(user): - """Resets the specified user's confirmation token and sends the user - an email with instructions explaining next steps. - - :param user: The user to work with - """ - token = generate_confirmation_token(user) - - user.confirmed_at = None - _datastore._save_model(user) - - send_confirmation_instructions(user, token) - - return token diff --git a/flask_security/core.py b/flask_security/core.py index 4af1d60..ec67e62 100644 --- a/flask_security/core.py +++ b/flask_security/core.py @@ -49,7 +49,6 @@ _default_config = { 'POST_REGISTER_VIEW': None, 'POST_CONFIRM_VIEW': None, 'POST_RESET_VIEW': None, - 'RESET_PASSWORD_ERROR_VIEW': None, 'UNAUTHORIZED_VIEW': None, 'DEFAULT_ROLES': [], 'CONFIRMABLE': False, diff --git a/flask_security/passwordless.py b/flask_security/passwordless.py index cc3332c..49394a4 100644 --- a/flask_security/passwordless.py +++ b/flask_security/passwordless.py @@ -16,7 +16,7 @@ from werkzeug.local import LocalProxy from .exceptions import PasswordlessLoginError from .signals import login_instructions_sent from .utils import send_mail, md5, get_max_age, login_user, get_message, \ - url_for_security + url_for_security, get_url # Convenient references @@ -47,6 +47,7 @@ def send_login_instructions(user, next): def generate_login_token(user, next): + next = next or get_url(_security.post_login_view) data = [user.id, md5(user.password), next] return _security.login_serializer.dumps(data) @@ -56,20 +57,19 @@ def login_by_token(token): max_age = get_max_age('LOGIN') try: - data = serializer.loads(token, max_age=max_age) - user = _datastore.find_user(id=data[0]) - + user_id, pw, next = serializer.loads(token, max_age=max_age) + user = _datastore.find_user(id=user_id) login_user(user, True) - - return user, data[2] + return user, next except SignatureExpired: sig_okay, data = serializer.loads_unsafe(token) + user_id, pw, next = data user = _datastore.find_user(id=data[0]) - msg = get_message('LOGIN_EXPIRED', - within=_security.login_within, - email=user.email)[0] - raise PasswordlessLoginError(msg, user=user, next=data[2]) + within = _security.login_within + msg = get_message('LOGIN_EXPIRED', within=within, email=user.email) + raise PasswordlessLoginError(msg[0], user=user, next=next) except BadSignature: - raise PasswordlessLoginError(get_message('INVALID_LOGIN_TOKEN')[0]) + msg = get_message('INVALID_LOGIN_TOKEN') + raise PasswordlessLoginError(msg[0]) diff --git a/flask_security/recoverable.py b/flask_security/recoverable.py index 15e34b6..d323894 100644 --- a/flask_security/recoverable.py +++ b/flask_security/recoverable.py @@ -13,9 +13,8 @@ from itsdangerous import BadSignature, SignatureExpired from flask import current_app as app, request from werkzeug.local import LocalProxy -from .exceptions import ResetPasswordError, UserNotFoundError -from .signals import password_reset, password_reset_requested, \ - reset_instructions_sent +from .exceptions import ResetPasswordError +from .signals import password_reset, reset_password_instructions_sent from .utils import send_mail, get_max_age, md5, get_message, encrypt_password, \ url_for_security @@ -26,12 +25,13 @@ _security = LocalProxy(lambda: app.extensions['security']) _datastore = LocalProxy(lambda: _security.datastore) -def send_reset_password_instructions(user, reset_token): +def send_reset_password_instructions(user): """Sends the reset password instructions email for the specified user. :param user: The user to send the instructions to """ - url = url_for_security('reset_password', token=reset_token) + token = generate_reset_password_token(user) + url = url_for_security('reset_password', token=token) reset_link = request.url_root[:-1] + url @@ -40,8 +40,8 @@ def send_reset_password_instructions(user, reset_token): 'reset_instructions', dict(user=user, reset_link=reset_link)) - reset_instructions_sent.send(dict(user=user, token=reset_token), - app=app._get_current_object()) + reset_password_instructions_sent.send(dict(user=user, token=token), + app=app._get_current_object()) def send_password_reset_notice(user): @@ -99,19 +99,3 @@ def reset_by_token(token, password): except BadSignature: raise ResetPasswordError(get_message('INVALID_RESET_PASSWORD_TOKEN')[0]) - - -def reset_password_reset_token(user): - """Resets the specified user's reset password token and sends the user - an email with instructions explaining next steps. - - :param user: The user to work with - """ - token = generate_reset_password_token(user) - - send_reset_password_instructions(user, token) - - password_reset_requested.send(dict(user=user, token=token), - app=app._get_current_object()) - - return token diff --git a/flask_security/signals.py b/flask_security/signals.py index dcf3091..17aac64 100644 --- a/flask_security/signals.py +++ b/flask_security/signals.py @@ -24,6 +24,4 @@ login_instructions_sent = signals.signal("login-instructions-sent") password_reset = signals.signal("password-reset") -password_reset_requested = signals.signal("password-reset-requested") - -reset_instructions_sent = signals.signal("reset-instructions-sent") +reset_password_instructions_sent = signals.signal("password-reset-instructions-sent") diff --git a/flask_security/utils.py b/flask_security/utils.py index 0ca0245..7041040 100644 --- a/flask_security/utils.py +++ b/flask_security/utils.py @@ -24,7 +24,7 @@ from flask.ext.principal import Identity, AnonymousIdentity, identity_changed from werkzeug.local import LocalProxy from .core import current_user -from .signals import user_registered, password_reset_requested, \ +from .signals import user_registered, reset_password_instructions_sent, \ login_instructions_sent @@ -292,9 +292,9 @@ def capture_reset_password_requests(reset_password_sent_at=None): def _on(request, app): reset_requests.append(request) - password_reset_requested.connect(_on) + reset_password_instructions_sent.connect(_on) try: yield reset_requests finally: - password_reset_requested.disconnect(_on) + reset_password_instructions_sent.disconnect(_on) diff --git a/flask_security/views.py b/flask_security/views.py index 60baed9..4b2a8fd 100644 --- a/flask_security/views.py +++ b/flask_security/views.py @@ -14,7 +14,8 @@ from flask import current_app as app, redirect, request, \ from werkzeug.datastructures import MultiDict from werkzeug.local import LocalProxy -from flask_security.confirmable import confirm_by_token, reset_confirmation_token +from flask_security.confirmable import confirm_by_token, \ + send_confirmation_instructions from flask_security.decorators import login_required from flask_security.exceptions import ConfirmationError, BadCredentialsError, \ ResetPasswordError, PasswordlessLoginError @@ -22,11 +23,11 @@ from flask_security.forms import LoginForm, RegisterForm, ForgotPasswordForm, \ ResetPasswordForm, SendConfirmationForm, PasswordlessLoginForm from flask_security.passwordless import send_login_instructions, login_by_token from flask_security.recoverable import reset_by_token, \ - reset_password_reset_token + send_reset_password_instructions from flask_security.signals import user_registered from flask_security.utils import get_url, get_post_login_redirect, do_flash, \ - get_message, config_value, login_user, logout_user, url_for_security, \ - anonymous_user_required + get_message, config_value, login_user, logout_user, \ + anonymous_user_required, url_for_security as url_for # Convenient references @@ -64,55 +65,61 @@ def _json_auth_error(msg): return resp -def _commit_data(response=None): +def _commit(response=None): _datastore._commit() return response +def _ctx(endpoint): + return _security._run_ctx_processor(endpoint) + + def authenticate(): """View function which handles an authentication request.""" - confirm_url = None - form_data = MultiDict(request.json) if request.json else request.form - form = LoginForm(form_data) - after_this_request(_commit_data) + form = LoginForm(request.form) + user, msg, confirm_url = None, None, None + + if request.json: + form = LoginForm(MultiDict(request.json)) try: user = _security.auth_provider.authenticate(form) - - if login_user(user, remember=form.remember.data): - if request.json: - return _json_auth_ok(user) - - return redirect(get_post_login_redirect()) - - raise BadCredentialsError(get_message('DISABLED_ACCOUNT')[0]) - except ConfirmationError, e: msg = str(e) - confirm_url = url_for_security('send_confirmation', email=e.user.email) - + confirm_url = url_for('send_confirmation', email=e.user.email) except BadCredentialsError, e: msg = str(e) + if user: + if login_user(user, remember=form.remember.data): + after_this_request(_commit) + if request.json: + return _json_auth_ok(user) + return redirect(get_post_login_redirect()) + msg = get_message('DISABLED_ACCOUNT')[0] + _logger.debug('Unsuccessful authentication attempt: %s' % msg) if request.json: return _json_auth_error(msg) do_flash(msg, 'error') - - return redirect(confirm_url or url_for_security('login')) + return redirect(confirm_url or url_for('login')) @anonymous_user_required def login(): """View function for login view""" - form = PasswordlessLoginForm() if _security.passwordless else LoginForm() - template = 'send_login' if _security.passwordless else 'login' - return render_template('security/%s.html' % template, login_form=form, - **_security._run_ctx_processor('login')) + tmp, form = '', LoginForm + + if _security.passwordless: + tmp, form = 'send_', PasswordlessLoginForm + + return render_template('security/%slogin.html' % tmp, + login_form=form(), + **_ctx('login')) @login_required @@ -120,11 +127,10 @@ def logout(): """View function which handles a logout request.""" logout_user() - _logger.debug('User logged out') - - return redirect(request.args.get('next', None) or - get_url(_security.post_logout_view)) + next_url = request.args.get('next', None) + post_logout_url = get_url(_security.post_logout_view) + return redirect(next_url or post_logout_url) @anonymous_user_required @@ -133,51 +139,51 @@ def register(): form = RegisterForm(csrf_enabled=not app.testing) - if form.validate_on_submit(): - # Create user - u = _datastore.create_user(**form.to_dict()) + if not form.validate_on_submit(): + return render_template('security/register.html', + register_user_form=form, + **_ctx('register')) - # Save the user so the ID is created - _commit_data() + token = None + user = _datastore.create_user(**form.to_dict()) + _commit() - # Send confirmation instructions if necessary - t = reset_confirmation_token(u) if _security.confirmable else None + if _security.confirmable: + token = send_confirmation_instructions(user) + do_flash(*get_message('CONFIRM_REGISTRATION', email=user.email)) - data = dict(user=u, confirm_token=t) - user_registered.send(data, app=app._get_current_object()) + user_registered.send(dict(user=user, confirm_token=token), + app=app._get_current_object()) - _logger.debug('User %s registered' % u) + _logger.debug('User %s registered' % user) - # Login the user if allowed - if not _security.confirmable or _security.login_without_confirmation: - login_user(u) + if not _security.confirmable or _security.login_without_confirmation: + after_this_request(_commit) + login_user(user) - if _security.confirmable: - do_flash(*get_message('CONFIRM_REGISTRATION', email=u.email)) + post_register_url = get_url(_security.post_register_view) + post_login_url = get_url(_security.post_login_view) - return redirect(get_url(_security.post_register_view) or - get_url(_security.post_login_view)) - - return render_template('security/register.html', - register_user_form=form, - **_security._run_ctx_processor('register')) + return redirect(post_register_url or post_login_url) @anonymous_user_required def send_login(): """View function that sends login instructions for passwordless login""" - form = PasswordlessLoginForm() + form = PasswordlessLoginForm() user = _datastore.find_user(**form.to_dict()) if user.is_active(): send_login_instructions(user, form.next.data) - do_flash(*get_message('LOGIN_EMAIL_SENT', email=user.email)) + msg = get_message('LOGIN_EMAIL_SENT', email=user.email) else: - do_flash(*get_message('DISABLED_ACCOUNT')) + msg = get_message('DISABLED_ACCOUNT') - return render_template('security/send_login.html', login_form=form, - **_security._run_ctx_processor('send_login')) + do_flash(*msg) + return render_template('security/send_login.html', + login_form=form, + **_ctx('send_login')) @anonymous_user_required @@ -186,18 +192,14 @@ def token_login(token): try: user, next = login_by_token(token) - except PasswordlessLoginError, e: if e.user: send_login_instructions(e.user, e.next) - do_flash(str(e), 'error') - - return redirect(request.referrer or url_for_security('login')) + return redirect(request.referrer or url_for('login')) do_flash(*get_message('PASSWORDLESS_LOGIN_SUCCESSFUL')) - - return redirect(next or get_url(_security.post_login_view)) + return redirect(next) @anonymous_user_required @@ -208,45 +210,35 @@ def send_confirmation(): if form.validate_on_submit(): user = _datastore.find_user(**form.to_dict()) - - reset_confirmation_token(user) - + send_confirmation_instructions(user) _logger.debug('%s request confirmation instructions' % user) - do_flash(*get_message('CONFIRMATION_REQUEST', email=user.email)) return render_template('security/send_confirmation.html', reset_confirmation_form=form, - **_security._run_ctx_processor('send_confirmation')) + **_ctx('send_confirmation')) def confirm_email(token): """View function which handles a email confirmation request.""" - after_this_request(_commit_data) + after_this_request(_commit) try: user = confirm_by_token(token) - _logger.debug('%s confirmed their email' % user) - except ConfirmationError, e: - msg = (str(e), 'error') - - _logger.debug('Confirmation error: ' + msg[0]) - + _logger.debug('Confirmation error: %s' % e) if e.user: - reset_confirmation_token(e.user) - - do_flash(*msg) - - return redirect(get_url(_security.confirm_error_view) or - url_for_security('send_confirmation')) + send_confirmation_instructions(e.user) + do_flash(str(e), 'error') + confirm_error_url = get_url(_security.confirm_error_view) + return redirect(confirm_error_url or url_for('send_confirmation')) + _logger.debug('%s confirmed their email' % user) do_flash(*get_message('EMAIL_CONFIRMED')) - login_user(user, True) - - return redirect(get_url(_security.post_confirm_view) or - get_url(_security.post_login_view)) + post_confirm_url = get_url(_security.post_confirm_view) + post_login_url = get_url(_security.post_login_view) + return redirect(post_confirm_url or post_login_url) @anonymous_user_required @@ -257,11 +249,8 @@ def forgot_password(): if form.validate_on_submit(): user = _datastore.find_user(**form.to_dict()) - - reset_password_reset_token(user) - + send_reset_password_instructions(user) _logger.debug('%s requested to reset their password' % user) - do_flash(*get_message('PASSWORD_RESET_REQUEST', email=user.email)) if _security.post_forgot_view: @@ -272,49 +261,42 @@ def forgot_password(): return render_template('security/forgot_password.html', forgot_password_form=form, - **_security._run_ctx_processor('forgot_password')) + **_ctx('forgot_password')) @anonymous_user_required def reset_password(token): """View function that handles a reset password request.""" + next, msg = None, None form = ResetPasswordForm(csrf_enabled=not app.testing) if form.validate_on_submit(): try: user = reset_by_token(token=token, **form.to_dict()) - - _logger.debug('%s reset their password' % user) - - do_flash(*get_message('PASSWORD_RESET')) - - login_user(user) - - return redirect(get_url(_security.post_reset_view) or - get_url(_security.post_login_view)) - + msg = get_message('PASSWORD_RESET') + next = (get_url(_security.post_reset_view) or + get_url(_security.post_login_view)) except ResetPasswordError, e: msg = (str(e), 'error') - - _logger.debug('Password reset error: ' + msg[0]) - if e.user: - reset_password_reset_token(e.user) - + send_reset_password_instructions(e.user) msg = get_message('PASSWORD_RESET_EXPIRED', within=_security.reset_password_within, email=e.user.email) + _logger.debug('Password reset error: ' + msg[0]) - do_flash(*msg) + do_flash(*msg) - if _security.reset_password_error_view: - return redirect(get_url(_security.reset_password_error_view)) + if next: + login_user(user) + _logger.debug('%s reset their password' % user) + return redirect(next) return render_template('security/reset_password.html', reset_password_form=form, reset_password_token=token, - **_security._run_ctx_processor('reset_password')) + **_ctx('reset_password')) def create_blueprint(app, name, import_name, **kwargs):