Heavy work on confirmation and reset

This commit is contained in:
Matt Wright
2012-05-22 18:08:38 -04:00
parent 67b806860e
commit 09aa7e113c
26 changed files with 319 additions and 109 deletions
+4
View File
@@ -114,6 +114,8 @@ def create_sqlalchemy_app(auth_config=None):
confirmation_token = db.Column(db.String(255))
confirmation_sent_at = db.Column(db.DateTime())
confirmed_at = db.Column(db.DateTime())
reset_password_token = db.Column(db.String(255))
reset_password_sent_at = db.Column(db.DateTime())
roles = db.relationship('Role', secondary=roles_users,
backref=db.backref('users', lazy='dynamic'))
@@ -147,6 +149,8 @@ def create_mongoengine_app(auth_config=None):
confirmation_token = db.StringField(max_length=255)
confirmation_sent_at = db.DateTimeField()
confirmed_at = db.DateTimeField()
reset_password_token = db.StringField(max_length=255)
reset_password_sent_at = db.DateTimeField()
roles = db.ListField(db.ReferenceField(Role), default=[])
Security(app, MongoEngineUserDatastore(db, User, Role))
+5 -5
View File
@@ -1,10 +1,10 @@
{% include "_messages.html" %}
{% include "_nav.html" %}
<form action="{{ url_for('flask_security.register') }}" method="POST" name="register_form">
{{ form.hidden_tag() }}
{{ form.email.label }} {{ form.email }}<br/>
{{ form.password.label }} {{ form.password }}<br/>
{{ form.password_confirm.label }} {{ form.password_confirm }}<br/>
{{ form.submit }}
{{ register_user_form.hidden_tag() }}
{{ register_user_form.email.label }} {{ register_user_form.email }}<br/>
{{ register_user_form.password.label }} {{ register_user_form.password }}<br/>
{{ register_user_form.password_confirm.label }} {{ register_user_form.password_confirm }}<br/>
{{ register_user_form.submit }}
</form>
<p>{{ content }}</p>
+11 -6
View File
@@ -3,17 +3,18 @@ from datetime import datetime
from flask import current_app, request, url_for
from flask.ext.security.exceptions import UserNotFoundError, \
ConfirmationError, ConfirmationExpiredError
ConfirmationError, TokenExpiredError
from flask.ext.security.utils import generate_token, send_mail
from werkzeug.local import LocalProxy
security = LocalProxy(lambda: current_app.security)
logger = LocalProxy(lambda: current_app.logger)
def find_user_by_confirmation_token(token):
if not token:
raise ConfirmationError('Unknown confirmation token')
raise ConfirmationError('Confirmation token required')
return security.datastore.find_user(confirmation_token=token)
@@ -70,14 +71,18 @@ def confirmation_token_is_expired(user):
def confirm_by_token(token):
user = find_user_by_confirmation_token(token)
try:
user = find_user_by_confirmation_token(token)
except UserNotFoundError:
raise ConfirmationError('Invalid confirmation token')
if confirmation_token_is_expired(user):
raise ConfirmationExpiredError(user=user)
raise TokenExpiredError(message='Confirmation token is expired',
user=user)
user.confirmation_token = None
user.confirmation_sent_at = None
user.confirmed_at = datetime.utcnow()
#user.confirmation_token = None
#user.confirmation_sent_at = None
security.datastore._save_model(user)
+32 -12
View File
@@ -33,21 +33,25 @@ _default_config = {
'AUTH_PROVIDER': 'flask.ext.security::AuthenticationProvider',
'LOGIN_FORM': 'flask.ext.security::LoginForm',
'REGISTER_FORM': 'flask.ext.security::RegisterForm',
'RESET_PASSWORD_FORM': 'flask.ext.security::ResetPasswordForm',
'FORGOT_PASSWORD_FORM': 'flask.ext.security::ForgotPasswordForm',
'AUTH_URL': '/auth',
'LOGOUT_URL': '/logout',
'REGISTER_URL': '/register',
'FORGOT_URL': '/forgot',
'RESET_URL': '/reset',
'CONFIRM_URL': '/confirm',
'LOGIN_VIEW': '/login',
'POST_LOGIN_VIEW': '/',
'POST_LOGOUT_VIEW': '/',
'POST_FORGOT_VIEW': '/',
'POST_REGISTER_VIEW': None,
'POST_CONFIRM_VIEW': None,
'RESET_PASSWORD_WITHIN': 10,
'DEFAULT_ROLES': [],
'LOGIN_WITHOUT_CONFIRMATION': False,
'CONFIRM_EMAIL': False,
'CONFIRM_EMAIL_WITHIN': '5 days',
'RESET_PASSWORD_WITHIN': '2 days',
'LOGIN_WITHOUT_CONFIRMATION': False,
'EMAIL_SENDER': 'no-reply@localhost'
}
@@ -198,7 +202,7 @@ class Security(object):
self.init_app(app, datastore, **kwargs)
def init_app(self, app, datastore,
registerable=True, recoverable=False, template_folder=None):
registerable=True, recoverable=True, template_folder=None):
"""Initializes the Flask-Security extension for the specified
application and datastore implentation.
@@ -227,25 +231,32 @@ class Security(object):
self.datastore = datastore
self.LoginForm = utils.get_class_from_string(app, 'LOGIN_FORM')
self.RegisterForm = utils.get_class_from_string(app, 'REGISTER_FORM')
self.ResetPasswordForm = utils.get_class_from_string(app, 'RESET_PASSWORD_FORM')
self.ForgotPasswordForm = utils.get_class_from_string(app, 'FORGOT_PASSWORD_FORM')
self.auth_url = utils.config_value(app, 'AUTH_URL')
self.logout_url = utils.config_value(app, 'LOGOUT_URL')
self.reset_url = utils.config_value(app, 'RESET_URL')
self.register_url = utils.config_value(app, 'REGISTER_URL')
self.confirm_url = utils.config_value(app, 'CONFIRM_URL')
self.forgot_url = utils.config_value(app, 'FORGOT_URL')
self.post_login_view = utils.config_value(app, 'POST_LOGIN_VIEW')
self.post_logout_view = utils.config_value(app, 'POST_LOGOUT_VIEW')
self.post_register_view = utils.config_value(app, 'POST_REGISTER_VIEW')
self.post_confirm_view = utils.config_value(app, 'POST_CONFIRM_VIEW')
self.reset_password_within = utils.config_value(app, 'RESET_PASSWORD_WITHIN')
self.post_forgot_view = utils.config_value(app, 'POST_FORGOT_VIEW')
self.default_roles = utils.config_value(app, "DEFAULT_ROLES")
self.login_without_confirmation = utils.config_value(app, 'LOGIN_WITHOUT_CONFIRMATION')
self.confirm_email = utils.config_value(app, 'CONFIRM_EMAIL')
self.email_sender = utils.config_value(app, 'EMAIL_SENDER')
self.confirm_email_within_text = utils.config_value(app, 'CONFIRM_EMAIL_WITHIN')
self.confirm_email_within_text = utils.config_value(app, 'CONFIRM_EMAIL_WITHIN')
values = self.confirm_email_within_text.split()
self.confirm_email_within = timedelta(**{values[1]: int(values[0])})
self.reset_password_within_text = utils.config_value(app, 'RESET_PASSWORD_WITHIN')
values = self.reset_password_within_text.split()
self.reset_password_within = timedelta(**{values[1]: int(values[0])})
identity_loaded.connect_via(app)(on_identity_loaded)
bp = Blueprint('flask_security', __name__, template_folder='templates')
@@ -257,29 +268,37 @@ class Security(object):
bp.route(self.logout_url,
endpoint='logout')(login_required(views.logout))
self.setup_register(bp) if registerable else None
self.setup_reset(bp) if recoverable else None
self.setup_confirm(bp) if self.confirm_email else None
self.setup_registerable(bp) if registerable else None
self.setup_recoverable(bp) if recoverable else None
self.setup_confirmable(bp) if self.confirm_email else None
app.register_blueprint(bp,
url_prefix=utils.config_value(app, 'URL_PREFIX'))
app.security = self
def setup_register(self, bp):
def setup_registerable(self, bp):
bp.route(self.register_url,
methods=['POST'],
endpoint='register')(views.register)
def setup_reset(self, bp):
def setup_recoverable(self, bp):
bp.route(self.forgot_url,
methods=['POST'],
endpoint='forgot')(views.forgot)
bp.route(self.reset_url,
methods=['POST'],
endpoint='reset')(views.reset)
def setup_confirm(self, bp):
def setup_confirmable(self, bp):
bp.route(self.confirm_url, endpoint='confirm')(views.confirm)
class ForgotPasswordForm(Form):
email = TextField("Email Address",
validators=[Required(message="Email not provided")])
class LoginForm(Form):
"""The default login form"""
@@ -311,7 +330,8 @@ class RegisterForm(Form):
class ResetPasswordForm(Form):
token = HiddenField()
reset_password_token = HiddenField(validators=[Required()])
email = HiddenField(validators=[Required()])
password = PasswordField("Password",
validators=[Required(message="Password not provided")])
password_confirm = PasswordField("Retype Password",
+1 -1
View File
@@ -99,7 +99,7 @@ class UserDatastore(object):
user = self._do_find_user(**kwargs)
if user:
return user
raise exceptions.UserNotFoundError()
raise exceptions.UserNotFoundError('Parameters=%s' % kwargs)
def find_role(self, role):
"""Returns a role based on its name.
+23 -18
View File
@@ -10,68 +10,73 @@
"""
class BadCredentialsError(Exception):
class SecurityError(Exception):
def __init__(self, message=None, user=None):
super(SecurityError, self).__init__(message)
self.user = user
class BadCredentialsError(SecurityError):
"""Raised when an authentication attempt fails due to an error with the
provided credentials.
"""
class AuthenticationError(Exception):
class AuthenticationError(SecurityError):
"""Raised when an authentication attempt fails due to invalid configuration
or an unknown reason.
"""
class UserNotFoundError(Exception):
class UserNotFoundError(SecurityError):
"""Raised by a user datastore when there is an attempt to find a user by
their identifier, often username or email, and the user is not found.
"""
class RoleNotFoundError(Exception):
class RoleNotFoundError(SecurityError):
"""Raised by a user datastore when there is an attempt to find a role and
the role cannot be found.
"""
class UserIdNotFoundError(Exception):
class UserIdNotFoundError(SecurityError):
"""Raised by a user datastore when there is an attempt to find a user by
ID and the user is not found.
"""
class UserDatastoreError(Exception):
class UserDatastoreError(SecurityError):
"""Raised when a user datastore experiences an unexpected error
"""
class UserCreationError(Exception):
class UserCreationError(SecurityError):
"""Raised when an error occurs when creating a user
"""
class RoleCreationError(Exception):
class RoleCreationError(SecurityError):
"""Raised when an error occurs when creating a role
"""
class ConfirmationError(Exception):
"""Raised when an unknown confirmation error occurs
class ConfirmationError(SecurityError):
"""Raised when an confirmation error occurs
"""
class ConfirmationExpiredError(Exception):
class TokenExpiredError(SecurityError):
"""Raised when a user attempts to confirm their email but their token
has expired
"""
def __init__(self, user=None):
super(ConfirmationExpiredError, self).__init__()
self.user = user
class ConfirmationRequiredError(Exception):
class ConfirmationRequiredError(SecurityError):
"""Raised when a user attempts to login but requires confirmation
"""
def __init__(self, user=None):
super(ConfirmationRequiredError, self).__init__()
self.user = user
class ResetPasswordError(SecurityError):
"""Raised when a password reset error occurs
"""
+68 -20
View File
@@ -1,34 +1,82 @@
from datetime import datetime, timedelta
from datetime import datetime
from flask import current_app
from flask.ext.security.utils import generate_token
from flask import current_app, request, url_for
from flask.ext.security.exceptions import ResetPasswordError, \
UserNotFoundError, TokenExpiredError
from flask.ext.security.signals import password_reset_requested
from flask.ext.security.utils import generate_token, send_mail
from werkzeug.local import LocalProxy
security = LocalProxy(lambda: current_app.security)
logger = LocalProxy(lambda: current_app.logger)
def reset_password_period_valid(user):
sent_at = user.reset_password_sent_at
reset_within = int(current_app.security.reset_password_within)
days_ago = datetime.utcnow() - timedelta(days=reset_within)
def find_user_by_reset_token(token):
if not token:
raise ResetPasswordError('Reset password token required')
return security.datastore.find_user(reset_password_token=token)
return (sent_at is not None) and \
(sent_at >= days_ago)
def send_reset_password_instructions(user):
url = url_for('flask_security.reset',
reset_token=user.reset_password_token)
reset_link = request.url_root[:-1] + url
send_mail('Password reset instructions',
user.email,
'reset_instructions',
dict(user=user, reset_link=reset_link))
return True
def generate_reset_password_token(user):
user.reset_password_token = generate_token()
user.reset_password_sent_at = datetime.utcnow()
current_app.security.datastore._save_model(user)
while True:
token = generate_token()
try:
find_user_by_reset_token(token)
except UserNotFoundError:
break
now = datetime.utcnow()
try:
user['reset_password_token'] = token
user['reset_password_token'] = now
except TypeError:
user.reset_password_token = token
user.reset_password_sent_at = now
return user
def clear_reset_password_token(user):
def password_reset_token_is_expired(user):
token_expires = datetime.utcnow() - security.reset_password_within
return user.reset_password_sent_at < token_expires
def reset_by_token(token, email, password):
try:
user = find_user_by_reset_token(token)
except UserNotFoundError:
raise ResetPasswordError('Invalid reset password token')
if password_reset_token_is_expired(user):
raise TokenExpiredError('Reset password token is expired', user)
user.reset_password_token = None
user.reset_password_sent_at = None
user.password = security.pwd_context.encrypt(password)
security.datastore._save_model(user)
return user
def send_reset_password_instructions():
pass
def should_generate_reset_token(user):
return (user.reset_password_token is None) or \
(not reset_password_period_valid(user))
def reset_password_reset_token(user):
security.datastore._save_model(generate_reset_password_token(user))
send_reset_password_instructions(user)
password_reset_requested.send(user, app=current_app._get_current_object())
+3 -1
View File
@@ -2,4 +2,6 @@ import blinker
signals = blinker.Namespace()
user_registered = signals.signal("user-register")
user_registered = signals.signal("user-registered")
password_reset_requested = signals.signal("password-reset-requested")
-7
View File
@@ -1,7 +0,0 @@
<form action="{{ url_for('flask_security.register') }}" method="POST" name="register_form">
{{ form.hidden_tag() }}
{{ form.email.label }} {{ form.email }}<br/>
{{ form.password.label }} {{ form.password }}<br/>
{{ form.password_confirm.label }} {{ form.password_confirm }}<br/>
{{ form.submit }}
</form>
-6
View File
@@ -1,6 +0,0 @@
<form action="{{ url_for('flask_security.reset') }}" method="POST" name="reset_form">
{{ form.hidden_tag() }}
{{ form.password.label }} {{ form.password }}<br/>
{{ form.password_confirm.label }} {{ form.password_confirm }}<br/>
{{ form.submit }}
</form>
@@ -0,0 +1 @@
Resend confirmation instructions...
@@ -0,0 +1 @@
<p><a href="{{ reset_link }}">Click here to reset your password</a></p>
@@ -0,0 +1,3 @@
Click the link below to reset your password:
{{ reset_link }}
@@ -0,0 +1 @@
<p>Your password has been reset</p>
@@ -0,0 +1 @@
Your password has been reset
@@ -0,0 +1,6 @@
<form action="{{ url_for('flask_security.reset') }}" method="POST" name="reset_password_form">
{{ reset_password_form.hidden_tag() }}
{{ reset_password_form.password.label }} {{ reset_password_form.password }}<br/>
{{ reset_password_form.password_confirm.label }} {{ reset_password_form.password_confirm }}<br/>
{{ reset_password_form.submit }}
</form>
@@ -0,0 +1,5 @@
<form action="{{ url_for('flask_security.forgot') }}" method="POST" name="forgot_password_form">
{{ forgot_password_form.hidden_tag() }}
{{ forgot_password_form.email.label }} {{ forgot_password_form.email }}
{{ forgot_password_form.submit }}
</form>
@@ -0,0 +1,8 @@
<form action="{{ url_for('flask_security.update_user') }}" method="POST" name="edit_user_form">
{{ edit_user_form.hidden_tag() }}
{{ edit_user_form.email.label }} {{ edit_user_form.email }}<br/>
{{ edit_user_form.password.label }} {{ edit_user_form.password }}<br/>
{{ edit_user_form.password_confirm.label }} {{ edit_user_form.password_confirm }}<br/>
{{ edit_user_form.current_password.label }} {{ edit_user_form.current_password }}<br/>
{{ edit_user_form.submit }}
</form>
@@ -0,0 +1,7 @@
<form action="{{ url_for('flask_security.register') }}" method="POST" name="register_user_form">
{{ register_user_form.hidden_tag() }}
{{ register_user_form.email.label }} {{ register_user_form.email }}<br/>
{{ register_user_form.password.label }} {{ register_user_form.password }}<br/>
{{ register_user_form.password_confirm.label }} {{ register_user_form.password_confirm }}<br/>
{{ register_user_form.submit }}
</form>
+31 -3
View File
@@ -16,7 +16,8 @@ from contextlib import contextmanager
from importlib import import_module
from flask import url_for, flash, current_app, request, session, render_template
from flask.ext.security.signals import user_registered
from flask.ext.security.signals import user_registered, password_reset_requested
from werkzeug.exceptions import BadRequest
def generate_token():
@@ -74,8 +75,9 @@ def send_mail(subject, recipient, template, context):
sender=current_app.security.email_sender,
recipients=[recipient])
msg.body = render_template('email/%s.txt' % template, **context)
msg.html = render_template('email/%s.html' % template, **context)
base = 'security/email'
msg.body = render_template('%s/%s.txt' % (base, template), **context)
msg.html = render_template('%s/%s.html' % (base, template), **context)
current_app.mail.send(msg)
@@ -97,3 +99,29 @@ def capture_registrations(confirmation_sent_at=None):
yield users
finally:
user_registered.disconnect(_on)
@contextmanager
def capture_reset_password_requests(reset_password_sent_at=None):
users = []
def _on(user, app):
if reset_password_sent_at:
user.reset_password_sent_at = reset_password_sent_at
current_app.security.datastore._save_model(user)
users.append(user)
password_reset_requested.connect(_on)
try:
yield users
finally:
password_reset_requested.disconnect(_on)
def get_arg_or_bad_request(context, name):
rv = context.get(name, None)
if not rv:
raise BadRequest()
return rv
+43 -13
View File
@@ -9,14 +9,16 @@
:license: MIT, see LICENSE for more details.
"""
from flask import current_app, redirect, request, session
from flask import current_app, redirect, request, session, render_template
from flask.ext.login import login_user, logout_user
from flask.ext.principal import Identity, AnonymousIdentity, identity_changed
from flask.ext.security.confirmable import confirm_by_token, \
confirmation_token_is_expired, requires_confirmation, \
reset_confirmation_token, send_confirmation_instructions
from flask.ext.security.exceptions import ConfirmationExpiredError, \
ConfirmationError, BadCredentialsError
from flask.ext.security.recoverable import reset_by_token, \
reset_password_reset_token
from flask.ext.security.exceptions import TokenExpiredError, UserNotFoundError, \
ConfirmationError, BadCredentialsError, ResetPasswordError
from flask.ext.security.utils import get_post_login_redirect, do_flash
from flask.ext.security.signals import user_registered
from werkzeug.local import LocalProxy
@@ -70,7 +72,7 @@ def authenticate():
do_flash(msg, 'error')
logger.debug('Unsuccessful authentication attempt: %s. ' % msg)
logger.debug('Unsuccessful authentication attempt: %s' % msg)
return redirect(request.referrer or security.login_manager.login_view)
@@ -129,15 +131,16 @@ def confirm():
"""View function which confirms a user's email address using a token taken
from the value of the `confirmation_token` query string argument.
"""
try:
token = request.args.get('confirmation_token', None)
confirm_by_token(token)
user = confirm_by_token(token)
except ConfirmationError, e:
do_flash(str(e), 'error')
return redirect('/') # TODO: Don't just redirect to root
except ConfirmationExpiredError, e:
except TokenExpiredError, e:
reset_confirmation_token(e.user)
msg = 'You did not confirm your email within %s. ' \
@@ -146,17 +149,44 @@ def confirm():
do_flash(msg, 'error')
return redirect('/')
return redirect('/') # TODO: Don't redirect to root
logger.debug('User %s confirmed' % user)
do_flash('Your email has been confirmed. You may now log in.', 'success')
return redirect(security.post_confirm_view or security.post_login_view)
def forgot():
form = security.ForgotPasswordForm(csrf_enabled=not current_app.testing)
if form.validate_on_submit():
try:
user = security.datastore.find_user(email=form.email.data)
reset_password_reset_token(user)
do_flash('Instructions to reset your password have been sent to %s' % user.email, 'success')
except UserNotFoundError:
do_flash('The email you provided could not be found', 'error')
return redirect(security.post_forgot_view)
return render_template('security/passwords/new.html', forgot_password_form=form)
def reset():
# user = something
# if reset_password_period_valid_for_user(user):
# user.reset_password_sent_at = datetime.utcnow()
# user.reset_password_token = token
# current_app.security.datastore._save_model(user)
pass
form = security.ResetPasswordForm(csrf_enabled=not current_app.testing)
if form.validate_on_submit():
try:
reset_by_token(token=form.reset_password_token.data,
email=form.email.data,
password=form.password.data)
except ResetPasswordError, e:
do_flash(str(e))
except TokenExpiredError, e:
do_flash('You did not reset your password within %s.' % security.reset_password_within_text)
return redirect(request.referrer)
+65 -17
View File
@@ -3,7 +3,8 @@
import unittest
from datetime import datetime, timedelta
from flask.ext.security.utils import capture_registrations
from flask.ext.security.utils import capture_registrations, \
capture_reset_password_requests
from example import app
@@ -33,8 +34,9 @@ class SecurityTest(unittest.TestCase):
follow_redirects=follow_redirects,
content_type=content_type or 'application/x-www-form-urlencoded')
def register(self, email, password, endpoint=None):
return self._post(endpoint or '/register')
def register(self, email, password='password'):
data = dict(email=email, password=password, password_confirm=password)
return self.client.post('/register', data=data, follow_redirects=True)
def authenticate(self, email, password, endpoint=None):
data = dict(email=email, password=password)
@@ -113,12 +115,6 @@ class DefaultSecurityTests(SecurityTest):
r = self._get('/admin', follow_redirects=True)
self.assertIn('<input id="next"', r.data)
def test_register_valid_user(self):
data = dict(email='dude@lp.com', password='password', password_confirm='password')
self.client.post('/register', data=data, follow_redirects=True)
r = self.authenticate('dude@lp.com', 'password')
self.assertIn('Hello dude@lp.com', r.data)
class ConfiguredURLTests(SecurityTest):
@@ -150,15 +146,20 @@ class ConfiguredURLTests(SecurityTest):
self.assertIn('Post Register', r.data)
class ConfirmationTests(SecurityTest):
class RegisterableTests(SecurityTest):
def test_register_valid_user(self):
data = dict(email='dude@lp.com', password='password', password_confirm='password')
self.client.post('/register', data=data, follow_redirects=True)
r = self.authenticate('dude@lp.com', 'password')
self.assertIn('Hello dude@lp.com', r.data)
class ConfirmableTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CONFIRM_EMAIL': True
}
def register(self, email, password='password'):
data = dict(email=email, password=password, password_confirm=password)
return self.client.post('/register', data=data, follow_redirects=True)
def test_register_sends_confirmation_email(self):
e = 'dude@lp.com'
with self.app.mail.record_messages() as outbox:
@@ -176,9 +177,24 @@ class ConfirmationTests(SecurityTest):
r = self.client.get('/confirm?confirmation_token=' + token, follow_redirects=True)
self.assertIn('Your email has been confirmed. You may now log in.', r.data)
def test_invalid_or_unprovided_token_when_confirming_email(self):
def test_confirm_email_twice_flashes_invalid_token_msg(self):
e = 'dude@lp.com'
with capture_registrations() as users:
self.register(e)
token = users[0].confirmation_token
self.client.get('/confirm?confirmation_token=' + token, follow_redirects=True)
r = self.client.get('/confirm?confirmation_token=' + token, follow_redirects=True)
self.assertIn('Invalid confirmation token', r.data)
def test_unprovided_token_when_confirming_email(self):
r = self.client.get('/confirm', follow_redirects=True)
self.assertIn('Unknown confirmation token', r.data)
self.assertIn('Confirmation token required', r.data)
def test_invalid_token_when_confirming_email(self):
r = self.client.get('/confirm?confirmation_token=invalid', follow_redirects=True)
self.assertIn('Invalid confirmation token', r.data)
def test_expired_confirmation_token_sends_email(self):
e = 'dude@lp.com'
@@ -202,7 +218,7 @@ class ConfirmationTests(SecurityTest):
self.assertIn(text, r.data)
class ConfirmationAndCanLoginTests(SecurityTest):
class LoginWithoutImmediateConfirmTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CONFIRM_EMAIL': True,
'SECURITY_LOGIN_WITHOUT_CONFIRMATION': True
@@ -216,6 +232,38 @@ class ConfirmationAndCanLoginTests(SecurityTest):
self.assertIn(e, r.data)
class RecoverableTests(SecurityTest):
def test_forgot_post_sends_email_and_sets_required_fields(self):
with capture_reset_password_requests() as users:
with self.app.mail.record_messages() as outbox:
self.client.post('/forgot', data=dict(email='joe@lp.com'))
self.assertEqual(len(outbox), 1)
self.assertIsNotNone(users[0].reset_password_token)
self.assertIsNotNone(users[0].reset_password_sent_at)
def test_forgot_password_invalid_email(self):
r = self.client.post('/forgot',
data=dict(email='larry@lp.com'),
follow_redirects=True)
self.assertIn('The email you provided could not be found', r.data)
def test_reset_password_with_valid_token(self):
u = None
with capture_reset_password_requests() as users:
r = self.client.post('/forgot', data=dict(email='joe@lp.com'))
u = users[0]
r = self.client.post('/reset', data={
'email': u.email,
'reset_password_token': u.reset_password_token,
'password': 'newpassword',
'password_confirm': 'newpassword'
})
r = self.authenticate('joe@lp.com', 'newpassword')
self.assertIn('Hello joe@lp.com', r.data)
class MongoEngineSecurityTests(DefaultSecurityTests):
def _create_app(self, auth_config):