mirror of
https://github.com/wassname/flask-security.git
synced 2026-08-11 11:18:40 +08:00
Use itsdangerous for activation and password reset tokens so they do not need to be stored in the database
This commit is contained in:
@@ -136,13 +136,8 @@ def create_sqlalchemy_app(auth_config=None):
|
||||
current_login_ip = db.Column(db.String(100))
|
||||
login_count = db.Column(db.Integer)
|
||||
active = db.Column(db.Boolean())
|
||||
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())
|
||||
authentication_token = db.Column(db.String(255))
|
||||
authentication_token_created_at = db.Column(db.DateTime())
|
||||
roles = db.relationship('Role', secondary=roles_users,
|
||||
backref=db.backref('users', lazy='dynamic'))
|
||||
|
||||
@@ -179,13 +174,8 @@ def create_mongoengine_app(auth_config=None):
|
||||
current_login_ip = db.StringField(max_length=100)
|
||||
login_count = db.IntField()
|
||||
active = db.BooleanField(default=True)
|
||||
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()
|
||||
authentication_token = db.StringField(max_length=255)
|
||||
authentication_token_created_at = db.DateTimeField()
|
||||
roles = db.ListField(db.ReferenceField(Role), default=[])
|
||||
|
||||
Security(app, MongoEngineUserDatastore(db, User, Role))
|
||||
|
||||
@@ -11,11 +11,12 @@
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from itsdangerous import BadSignature, SignatureExpired
|
||||
from flask import current_app as app, request, url_for
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
from .exceptions import UserNotFoundError, ConfirmationError, TokenExpiredError
|
||||
from .utils import generate_token, send_mail, get_within_delta
|
||||
from .utils import send_mail, get_max_age, md5
|
||||
from .signals import user_confirmed, confirm_instructions_sent
|
||||
|
||||
|
||||
@@ -35,13 +36,13 @@ def find_user_by_confirmation_token(token):
|
||||
return _datastore.find_user(confirmation_token=token)
|
||||
|
||||
|
||||
def send_confirmation_instructions(user):
|
||||
def send_confirmation_instructions(user, token):
|
||||
"""Sends the confirmation instructions email for the specified user.
|
||||
|
||||
:param user: The user to send the instructions to
|
||||
"""
|
||||
url = url_for('flask_security.confirm',
|
||||
confirmation_token=user.confirmation_token)
|
||||
token=token)
|
||||
|
||||
confirmation_link = request.url_root[:-1] + url
|
||||
|
||||
@@ -59,23 +60,8 @@ def generate_confirmation_token(user):
|
||||
|
||||
:param user: The user to work with
|
||||
"""
|
||||
while True:
|
||||
token = generate_token()
|
||||
try:
|
||||
find_user_by_confirmation_token(token)
|
||||
except UserNotFoundError:
|
||||
break
|
||||
|
||||
now = datetime.utcnow()
|
||||
|
||||
try:
|
||||
user['confirmation_token'] = token
|
||||
user['confirmation_sent_at'] = now
|
||||
except TypeError:
|
||||
user.confirmation_token = token
|
||||
user.confirmation_sent_at = now
|
||||
|
||||
return user
|
||||
data = [user.id, md5(user.email)]
|
||||
return _security.confirm_serializer.dumps(data)
|
||||
|
||||
|
||||
def should_confirm_email(fn):
|
||||
@@ -93,13 +79,6 @@ def requires_confirmation(user):
|
||||
return user.confirmed_at == None
|
||||
|
||||
|
||||
@should_confirm_email
|
||||
def confirmation_token_is_expired(user):
|
||||
"""Returns `True` if the user's confirmation token is expired."""
|
||||
token_expires = datetime.utcnow() - get_within_delta('CONFIRM_EMAIL_WITHIN')
|
||||
return user.confirmation_sent_at < token_expires
|
||||
|
||||
|
||||
def confirm_by_token(token):
|
||||
"""Confirm the user given the specified token. If the token is invalid or
|
||||
the user is already confirmed a `ConfirmationError` error will be raised.
|
||||
@@ -107,26 +86,36 @@ def confirm_by_token(token):
|
||||
|
||||
:param token: The user's confirmation token
|
||||
"""
|
||||
serializer = _security.confirm_serializer
|
||||
max_age = get_max_age('CONFIRM_EMAIL')
|
||||
|
||||
try:
|
||||
user = find_user_by_confirmation_token(token)
|
||||
data = serializer.loads(token, max_age=max_age)
|
||||
user = _datastore.find_user(id=data[0])
|
||||
|
||||
if md5(user.email) != data[1]:
|
||||
raise UserNotFoundError()
|
||||
|
||||
except UserNotFoundError:
|
||||
raise ConfirmationError('Invalid confirmation token')
|
||||
|
||||
except SignatureExpired:
|
||||
sig_okay, data = serializer.loads_unsafe(token)
|
||||
user = _datastore.find_user(id=data[0])
|
||||
raise TokenExpiredError(message='Confirmation token is expired',
|
||||
user=user)
|
||||
|
||||
except BadSignature:
|
||||
raise ConfirmationError('Invalid confirmation token')
|
||||
|
||||
if user.confirmed_at:
|
||||
raise ConfirmationError('Account has already been confirmed')
|
||||
|
||||
if confirmation_token_is_expired(user):
|
||||
raise TokenExpiredError(message='Confirmation token is expired',
|
||||
user=user)
|
||||
|
||||
# TODO: Clear confirmation_token after confirmation?
|
||||
#user.confirmation_token = None
|
||||
#user.confirmation_sent_at = None
|
||||
user.confirmed_at = datetime.utcnow()
|
||||
|
||||
_datastore._save_model(user)
|
||||
|
||||
user_confirmed.send(user, app=app._get_current_object())
|
||||
|
||||
return user
|
||||
|
||||
|
||||
@@ -136,5 +125,11 @@ def reset_confirmation_token(user):
|
||||
|
||||
:param user: The user to work with
|
||||
"""
|
||||
_datastore._save_model(generate_confirmation_token(user))
|
||||
send_confirmation_instructions(user)
|
||||
token = generate_confirmation_token(user)
|
||||
|
||||
user.confirmed_at = None
|
||||
_datastore._save_model(user)
|
||||
|
||||
send_confirmation_instructions(user, token)
|
||||
|
||||
return token
|
||||
|
||||
+29
-9
@@ -9,6 +9,7 @@
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from itsdangerous import URLSafeTimedSerializer
|
||||
from flask import current_app, Blueprint
|
||||
from flask.ext.login import AnonymousUser as AnonymousUserBase, \
|
||||
UserMixin as BaseUserMixin, LoginManager, current_user
|
||||
@@ -18,8 +19,7 @@ from passlib.context import CryptContext
|
||||
from werkzeug.datastructures import ImmutableList
|
||||
|
||||
from . import views, exceptions
|
||||
from .confirmable import confirmation_token_is_expired, requires_confirmation, \
|
||||
reset_confirmation_token
|
||||
from .confirmable import requires_confirmation, reset_confirmation_token
|
||||
from .decorators import login_required
|
||||
from .utils import config_value as cv, get_config
|
||||
|
||||
@@ -33,8 +33,8 @@ _default_config = {
|
||||
'LOGOUT_URL': '/logout',
|
||||
'REGISTER_URL': '/register',
|
||||
'FORGOT_URL': '/forgot',
|
||||
'RESET_URL': '/reset',
|
||||
'CONFIRM_URL': '/confirm',
|
||||
'RESET_URL': '/reset/<token>',
|
||||
'CONFIRM_URL': '/confirm/<token>',
|
||||
'LOGIN_VIEW': '/login',
|
||||
'POST_LOGIN_VIEW': '/',
|
||||
'POST_LOGOUT_VIEW': '/',
|
||||
@@ -48,11 +48,14 @@ _default_config = {
|
||||
'RECOVERABLE': False,
|
||||
'TRACKABLE': False,
|
||||
'CONFIRM_EMAIL_WITHIN': '5 days',
|
||||
'RESET_PASSWORD_WITHIN': '2 days',
|
||||
'RESET_PASSWORD_WITHIN': '5 days',
|
||||
'LOGIN_WITHOUT_CONFIRMATION': False,
|
||||
'EMAIL_SENDER': 'no-reply@localhost',
|
||||
'TOKEN_AUTHENTICATION_KEY': 'auth_token',
|
||||
'TOKEN_AUTHENTICATION_HEADER': 'X-Auth-Token'
|
||||
'TOKEN_AUTHENTICATION_HEADER': 'X-Auth-Token',
|
||||
'CONFIRM_SALT': 'confirm-salt',
|
||||
'RESET_SALT': 'reset-salt',
|
||||
'AUTH_SALT': 'auth-salt'
|
||||
}
|
||||
|
||||
|
||||
@@ -109,6 +112,23 @@ def _get_pwd_context(app):
|
||||
return CryptContext(schemes=[pw_hash], default=pw_hash)
|
||||
|
||||
|
||||
def _get_serializer(app, salt):
|
||||
secret_key = app.config.get('SECRET_KEY', 'secret-key')
|
||||
return URLSafeTimedSerializer(secret_key=secret_key, salt=salt)
|
||||
|
||||
|
||||
def _get_reset_serializer(app):
|
||||
return _get_serializer(app, app.config['SECURITY_RESET_SALT'])
|
||||
|
||||
|
||||
def _get_confirm_serializer(app):
|
||||
return _get_serializer(app, app.config['SECURITY_CONFIRM_SALT'])
|
||||
|
||||
|
||||
def _get_token_auth_serializer(app):
|
||||
return _get_serializer(app, app.config['SECURITY_AUTH_SALT'])
|
||||
|
||||
|
||||
def _create_blueprint(app):
|
||||
bp = Blueprint('flask_security', __name__, template_folder='templates')
|
||||
|
||||
@@ -212,6 +232,9 @@ class Security(object):
|
||||
self.login_manager = _get_login_manager(app)
|
||||
self.principal = _get_principal(app)
|
||||
self.pwd_context = _get_pwd_context(app)
|
||||
self.reset_serializer = _get_reset_serializer(app)
|
||||
self.confirm_serializer = _get_confirm_serializer(app)
|
||||
self.token_auth_serializer = _get_token_auth_serializer(app)
|
||||
|
||||
for key, value in get_config(app).items():
|
||||
setattr(self, key.lower(), value)
|
||||
@@ -269,9 +292,6 @@ class AuthenticationProvider(object):
|
||||
except Exception, e:
|
||||
self.auth_error('Unexpected authentication error: %s' % e)
|
||||
|
||||
if confirmation_token_is_expired(user):
|
||||
reset_confirmation_token(user)
|
||||
|
||||
if requires_confirmation(user):
|
||||
raise exceptions.BadCredentialsError('Account requires confirmation')
|
||||
|
||||
|
||||
@@ -90,9 +90,6 @@ class UserDatastore(object):
|
||||
kwargs.setdefault('active', True)
|
||||
kwargs.setdefault('roles', current_app.security.default_roles)
|
||||
|
||||
if current_app.security.confirmable:
|
||||
confirmable.generate_confirmation_token(kwargs)
|
||||
|
||||
if email is None:
|
||||
raise exceptions.UserCreationError('Missing email argument')
|
||||
|
||||
|
||||
+1
-13
@@ -66,23 +66,11 @@ class RegisterForm(Form,
|
||||
|
||||
|
||||
class ResetPasswordForm(Form,
|
||||
EmailFormMixin,
|
||||
PasswordFormMixin,
|
||||
PasswordConfirmFormMixin):
|
||||
"""The default reset password form"""
|
||||
|
||||
token = HiddenField(validators=[Required()])
|
||||
|
||||
submit = SubmitField("Reset Password")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ResetPasswordForm, self).__init__(*args, **kwargs)
|
||||
|
||||
if request.method == 'GET':
|
||||
self.token.data = request.args.get('token', None)
|
||||
self.email.data = request.args.get('email', None)
|
||||
|
||||
def to_dict(self):
|
||||
return dict(token=self.token.data,
|
||||
email=self.email.data,
|
||||
password=self.password.data)
|
||||
return dict(password=self.password.data)
|
||||
|
||||
@@ -9,16 +9,15 @@
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from itsdangerous import BadSignature, SignatureExpired
|
||||
from flask import current_app as app, request, url_for
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
from .exceptions import ResetPasswordError, UserNotFoundError, \
|
||||
TokenExpiredError
|
||||
from .signals import password_reset, password_reset_requested, \
|
||||
confirm_instructions_sent
|
||||
from .utils import generate_token, send_mail, get_within_delta
|
||||
reset_instructions_sent
|
||||
from .utils import send_mail, get_max_age, md5
|
||||
|
||||
|
||||
# Convenient references
|
||||
@@ -27,24 +26,13 @@ _security = LocalProxy(lambda: app.security)
|
||||
_datastore = LocalProxy(lambda: app.security.datastore)
|
||||
|
||||
|
||||
def find_user_by_reset_token(token):
|
||||
"""Returns a user with a matching reset password token.
|
||||
|
||||
:param token: The reset password token
|
||||
"""
|
||||
if not token:
|
||||
raise ResetPasswordError('Reset password token required')
|
||||
return _datastore.find_user(reset_password_token=token)
|
||||
|
||||
|
||||
def send_reset_password_instructions(user):
|
||||
def send_reset_password_instructions(user, reset_token):
|
||||
"""Sends the reset password instructions email for the specified user.
|
||||
|
||||
:param user: The user to send the instructions to
|
||||
"""
|
||||
url = url_for('flask_security.reset',
|
||||
email=user.email,
|
||||
reset_token=user.reset_password_token)
|
||||
token=reset_token)
|
||||
|
||||
reset_link = request.url_root[:-1] + url
|
||||
|
||||
@@ -53,45 +41,33 @@ def send_reset_password_instructions(user):
|
||||
'reset_instructions',
|
||||
dict(user=user, reset_link=reset_link))
|
||||
|
||||
confirm_instructions_sent.send(user, app=app._get_current_object())
|
||||
reset_instructions_sent.send(dict(user=user, token=reset_token),
|
||||
app=app._get_current_object())
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def send_password_reset_notice(user):
|
||||
"""Sends the password reset notice email for the specified user.
|
||||
|
||||
:param user: The user to send the notice to
|
||||
"""
|
||||
send_mail('Your password has been reset',
|
||||
user.email,
|
||||
'reset_notice',
|
||||
dict(user=user))
|
||||
|
||||
|
||||
def generate_reset_password_token(user):
|
||||
"""Generates a unique reset password token for the specified user.
|
||||
|
||||
:param user: The user to work with
|
||||
"""
|
||||
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_sent_at'] = now
|
||||
except TypeError:
|
||||
user.reset_password_token = token
|
||||
user.reset_password_sent_at = now
|
||||
|
||||
return user
|
||||
data = [user.id, md5(user.password)]
|
||||
return _security.reset_serializer.dumps(data)
|
||||
|
||||
|
||||
def password_reset_token_is_expired(user):
|
||||
"""Returns `True` if the specified user's reset password token is expired.
|
||||
|
||||
:param user: The user to examine
|
||||
"""
|
||||
token_expires = datetime.utcnow() - get_within_delta('RESET_PASSWORD_WITHIN')
|
||||
return user.reset_password_sent_at < token_expires
|
||||
|
||||
|
||||
def reset_by_token(token, email, password):
|
||||
def reset_by_token(token, password):
|
||||
"""Resets the password of the user given the specified token, email and
|
||||
password. If the token is invalid a `ResetPasswordError` error will be
|
||||
raised. If the token is expired a `TokenExpiredError` error will be raised.
|
||||
@@ -100,21 +76,32 @@ def reset_by_token(token, email, password):
|
||||
:param email: The user's email address
|
||||
:param password: The user's new password
|
||||
"""
|
||||
serializer = _security.reset_serializer
|
||||
max_age = get_max_age('RESET_PASSWORD')
|
||||
|
||||
try:
|
||||
user = find_user_by_reset_token(token)
|
||||
data = serializer.loads(token, max_age=max_age)
|
||||
user = _datastore.find_user(id=data[0])
|
||||
|
||||
if md5(user.password) != data[1]:
|
||||
raise UserNotFoundError()
|
||||
|
||||
except UserNotFoundError:
|
||||
raise ResetPasswordError('Invalid reset password token')
|
||||
|
||||
if password_reset_token_is_expired(user):
|
||||
except SignatureExpired:
|
||||
sig_okay, data = serializer.loads_unsafe(token)
|
||||
user = _datastore.find_user(id=data[0])
|
||||
raise TokenExpiredError('Reset password token is expired', user)
|
||||
|
||||
user.reset_password_token = None
|
||||
user.reset_password_sent_at = None
|
||||
except BadSignature:
|
||||
raise ResetPasswordError('Invalid reset password token')
|
||||
|
||||
user.password = _security.pwd_context.encrypt(password)
|
||||
|
||||
_datastore._save_model(user)
|
||||
|
||||
send_mail('Your password has been reset', user.email, 'reset_notice')
|
||||
send_password_reset_notice(user)
|
||||
|
||||
password_reset.send(user, app=app._get_current_object())
|
||||
|
||||
@@ -127,6 +114,11 @@ def reset_password_reset_token(user):
|
||||
|
||||
:param user: The user to work with
|
||||
"""
|
||||
_datastore._save_model(generate_reset_password_token(user))
|
||||
send_reset_password_instructions(user)
|
||||
password_reset_requested.send(user, app=app._get_current_object())
|
||||
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
|
||||
|
||||
+21
-18
@@ -10,9 +10,10 @@
|
||||
"""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from flask import url_for, flash, current_app, request, session, render_template
|
||||
from flask.ext.login import make_secure_token
|
||||
@@ -20,6 +21,10 @@ from flask.ext.login import make_secure_token
|
||||
from .signals import user_registered, password_reset_requested
|
||||
|
||||
|
||||
def md5(data):
|
||||
return hashlib.md5(data).hexdigest()
|
||||
|
||||
|
||||
def generate_token():
|
||||
"""Generate an arbitrary URL safe token."""
|
||||
return base64.urlsafe_b64encode(os.urandom(30))
|
||||
@@ -101,6 +106,12 @@ def config_value(key, app=None, default=None):
|
||||
return get_config(app).get(key.upper(), default)
|
||||
|
||||
|
||||
def get_max_age(key, app=None):
|
||||
now = datetime.utcnow()
|
||||
expires = now + get_within_delta(key + '_WITHIN', app)
|
||||
return int(expires.strftime('%s')) - int(now.strftime('%s'))
|
||||
|
||||
|
||||
def get_within_delta(key, app=None):
|
||||
"""Get a timedelta object from the application configuration following
|
||||
the internal convention of::
|
||||
@@ -145,25 +156,21 @@ def send_mail(subject, recipient, template, context=None):
|
||||
|
||||
|
||||
@contextmanager
|
||||
def capture_registrations(confirmation_sent_at=None):
|
||||
def capture_registrations():
|
||||
"""Testing utility for capturing registrations.
|
||||
|
||||
:param confirmation_sent_at: An optional datetime object to set the
|
||||
user's `confirmation_sent_at` to
|
||||
"""
|
||||
users = []
|
||||
registrations = []
|
||||
|
||||
def _on(user, app):
|
||||
if confirmation_sent_at:
|
||||
user.confirmation_sent_at = confirmation_sent_at
|
||||
current_app.security.datastore._save_model(user)
|
||||
|
||||
users.append(user)
|
||||
def _on(data, app):
|
||||
registrations.append(data)
|
||||
|
||||
user_registered.connect(_on)
|
||||
|
||||
try:
|
||||
yield users
|
||||
yield registrations
|
||||
finally:
|
||||
user_registered.disconnect(_on)
|
||||
|
||||
@@ -175,18 +182,14 @@ def capture_reset_password_requests(reset_password_sent_at=None):
|
||||
:param reset_password_sent_at: An optional datetime object to set the
|
||||
user's `reset_password_sent_at` to
|
||||
"""
|
||||
users = []
|
||||
reset_requests = []
|
||||
|
||||
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)
|
||||
def _on(request, app):
|
||||
reset_requests.append(request)
|
||||
|
||||
password_reset_requested.connect(_on)
|
||||
|
||||
try:
|
||||
yield users
|
||||
yield reset_requests
|
||||
finally:
|
||||
password_reset_requested.disconnect(_on)
|
||||
|
||||
+10
-10
@@ -17,8 +17,7 @@ from flask.ext.login import login_user, logout_user
|
||||
from flask.ext.principal import Identity, AnonymousIdentity, identity_changed
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
from .confirmable import confirm_by_token, \
|
||||
reset_confirmation_token, send_confirmation_instructions
|
||||
from .confirmable import confirm_by_token, reset_confirmation_token
|
||||
from .exceptions import TokenExpiredError, UserNotFoundError, \
|
||||
ConfirmationError, BadCredentialsError, ResetPasswordError
|
||||
from .forms import LoginForm, RegisterForm, ForgotPasswordForm, \
|
||||
@@ -116,12 +115,14 @@ def register():
|
||||
if form.validate_on_submit():
|
||||
# Create user and send signal
|
||||
user = _datastore.create_user(**form.to_dict())
|
||||
|
||||
user_registered.send(user, app=app._get_current_object())
|
||||
confirm_token = None
|
||||
|
||||
# Send confirmation instructions if necessary
|
||||
if _security.confirmable:
|
||||
send_confirmation_instructions(user)
|
||||
confirm_token = reset_confirmation_token(user)
|
||||
|
||||
user_registered.send(dict(user=user, confirm_token=confirm_token),
|
||||
app=app._get_current_object())
|
||||
|
||||
_logger.debug('User %s registered' % user)
|
||||
|
||||
@@ -136,11 +137,10 @@ def register():
|
||||
_security.register_url)
|
||||
|
||||
|
||||
def confirm():
|
||||
def confirm(token):
|
||||
"""View function which handles a account confirmation request."""
|
||||
|
||||
try:
|
||||
token = request.args.get('confirmation_token', None)
|
||||
user = confirm_by_token(token)
|
||||
|
||||
except ConfirmationError, e:
|
||||
@@ -187,21 +187,21 @@ def forgot():
|
||||
forgot_password_form=form)
|
||||
|
||||
|
||||
def reset():
|
||||
def reset(token):
|
||||
"""View function that handles a reset password request."""
|
||||
|
||||
form = ResetPasswordForm(csrf_enabled=not app.testing)
|
||||
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
reset_by_token(**form.to_dict())
|
||||
reset_by_token(token=token, **form.to_dict())
|
||||
|
||||
except ResetPasswordError, e:
|
||||
do_flash(str(e), 'error')
|
||||
|
||||
except TokenExpiredError, e:
|
||||
do_flash('You did not reset your password within'
|
||||
'%s.' % _security.reset_password_within_text)
|
||||
'%s.' % _security.reset_password_within)
|
||||
|
||||
return redirect(request.referrer or
|
||||
_security.reset_password_error_view)
|
||||
|
||||
+46
-37
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from flask.ext.security.utils import capture_registrations, \
|
||||
@@ -157,7 +158,8 @@ class RegisterableTests(SecurityTest):
|
||||
class ConfirmableTests(SecurityTest):
|
||||
AUTH_CONFIG = {
|
||||
'SECURITY_CONFIRMABLE': True,
|
||||
'SECURITY_REGISTERABLE': True
|
||||
'SECURITY_REGISTERABLE': True,
|
||||
'SECURITY_CONFIRM_EMAIL_WITHIN': '1 seconds'
|
||||
}
|
||||
|
||||
def test_register_sends_confirmation_email(self):
|
||||
@@ -170,44 +172,40 @@ class ConfirmableTests(SecurityTest):
|
||||
def test_confirm_email(self):
|
||||
e = 'dude@lp.com'
|
||||
|
||||
with capture_registrations() as users:
|
||||
with capture_registrations() as registrations:
|
||||
self.register(e)
|
||||
token = users[0].confirmation_token
|
||||
token = registrations[0]['confirm_token']
|
||||
|
||||
r = self.client.get('/confirm?confirmation_token=' + token, follow_redirects=True)
|
||||
r = self.client.get('/confirm/' + token, follow_redirects=True)
|
||||
self.assertIn('Your email has been confirmed. You may now log in.', r.data)
|
||||
|
||||
def test_confirm_email_twice_flashes_invalid_token_msg(self):
|
||||
def test_confirm_email_twice_flashes_already_confirmed_message(self):
|
||||
e = 'dude@lp.com'
|
||||
|
||||
with capture_registrations() as users:
|
||||
with capture_registrations() as registrations:
|
||||
self.register(e)
|
||||
token = users[0].confirmation_token
|
||||
token = registrations[0]['confirm_token']
|
||||
|
||||
url = '/confirm?confirmation_token=' + token
|
||||
url = '/confirm/' + token
|
||||
self.client.get(url, follow_redirects=True)
|
||||
r = self.client.get(url, follow_redirects=True)
|
||||
self.assertIn('Account has already been confirmed', r.data)
|
||||
|
||||
def test_unprovided_token_when_confirming_email(self):
|
||||
r = self.client.get('/confirm', follow_redirects=True)
|
||||
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)
|
||||
r = self.client.get('/confirm/bogus', follow_redirects=True)
|
||||
self.assertIn('Invalid confirmation token', r.data)
|
||||
|
||||
def test_expired_confirmation_token_sends_email(self):
|
||||
e = 'dude@lp.com'
|
||||
|
||||
sent_at = datetime.utcnow() - timedelta(days=15)
|
||||
|
||||
with capture_registrations(confirmation_sent_at=sent_at) as users:
|
||||
with capture_registrations() as registrations:
|
||||
self.register(e)
|
||||
token = users[0].confirmation_token
|
||||
token = registrations[0]['confirm_token']
|
||||
|
||||
time.sleep(3)
|
||||
|
||||
with self.app.mail.record_messages() as outbox:
|
||||
r = self.client.get('/confirm?confirmation_token=' + token, follow_redirects=True)
|
||||
r = self.client.get('/confirm/' + token, follow_redirects=True)
|
||||
|
||||
self.assertEqual(len(outbox), 1)
|
||||
self.assertIn(e, outbox[0].html)
|
||||
@@ -237,16 +235,15 @@ class LoginWithoutImmediateConfirmTests(SecurityTest):
|
||||
class RecoverableTests(SecurityTest):
|
||||
|
||||
AUTH_CONFIG = {
|
||||
'SECURITY_RECOVERABLE': True
|
||||
'SECURITY_RECOVERABLE': True,
|
||||
'SECURITY_RESET_PASSWORD_WITHIN': '1 seconds'
|
||||
}
|
||||
|
||||
def test_forgot_post_sends_email_and_sets_required_fields(self):
|
||||
with capture_reset_password_requests() as users:
|
||||
def test_forgot_post_sends_email(self):
|
||||
with capture_reset_password_requests():
|
||||
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',
|
||||
@@ -255,35 +252,47 @@ class RecoverableTests(SecurityTest):
|
||||
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:
|
||||
with capture_reset_password_requests() as requests:
|
||||
r = self.client.post('/forgot', data=dict(email='joe@lp.com'))
|
||||
u = users[0]
|
||||
t = requests[0]['token']
|
||||
|
||||
r = self.client.post('/reset', data={
|
||||
'email': u.email,
|
||||
'token': u.reset_password_token,
|
||||
r = self.client.post('/reset/' + t, data={
|
||||
'password': 'newpassword',
|
||||
'password_confirm': 'newpassword'
|
||||
})
|
||||
|
||||
r = self.authenticate('joe@lp.com', 'newpassword')
|
||||
self.assertIn('Hello joe@lp.com', r.data)
|
||||
|
||||
def test_reset_password_with_expired_token(self):
|
||||
with capture_reset_password_requests() as requests:
|
||||
r = self.client.post('/forgot',
|
||||
data=dict(email='joe@lp.com'),
|
||||
follow_redirects=True)
|
||||
t = requests[0]['token']
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
r = self.client.post('/reset/' + t, data={
|
||||
'password': 'newpassword',
|
||||
'password_confirm': 'newpassword'
|
||||
}, follow_redirects=True)
|
||||
|
||||
self.assertIn('You did not reset your password within', r.data)
|
||||
|
||||
def test_reset_password_twice_flashes_invalid_token_msg(self):
|
||||
u = None
|
||||
with capture_reset_password_requests() as users:
|
||||
r = self.client.post('/forgot', data=dict(email='joe@lp.com'))
|
||||
u = users[0]
|
||||
with capture_reset_password_requests() as requests:
|
||||
self.client.post('/forgot', data=dict(email='joe@lp.com'))
|
||||
t = requests[0]['token']
|
||||
|
||||
data = {
|
||||
'email': u.email,
|
||||
'token': u.reset_password_token,
|
||||
'password': 'newpassword',
|
||||
'password_confirm': 'newpassword'
|
||||
}
|
||||
|
||||
self.client.post('/reset', data=data)
|
||||
r = self.client.post('/reset', data=data, follow_redirects=True)
|
||||
url = '/reset/' + t
|
||||
r = self.client.post(url, data=data, follow_redirects=True)
|
||||
r = self.client.post(url, data=data, follow_redirects=True)
|
||||
self.assertIn('Invalid reset password token', r.data)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user