mirror of
https://github.com/wassname/flask-security.git
synced 2026-06-27 16:10:11 +08:00
Heavy work on confirmation and reset
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -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
@@ -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
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user