mirror of
https://github.com/wassname/flask-security.git
synced 2026-08-07 11:22:21 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e773232e5a | ||
|
|
f401eac496 | ||
|
|
f6b532103d | ||
|
|
27a4bbac6a | ||
|
|
0cf7d563dc | ||
|
|
7c906de427 | ||
|
|
9c03884441 | ||
|
|
4ed4579738 | ||
|
|
156ccaecc1 | ||
|
|
a5482922f3 | ||
|
|
d3cfddfcac | ||
|
|
312a527498 | ||
|
|
d87765fc3b | ||
|
|
d05e699a15 | ||
|
|
e7855488df | ||
|
|
01bdc0d356 | ||
|
|
5e2a016bcd | ||
|
|
0f46f35981 | ||
|
|
c0d9eecf10 | ||
|
|
04bb2c4041 | ||
|
|
8eeb832d2e | ||
|
|
8f760aadbd | ||
|
|
03d27cd600 | ||
|
|
d30a27b3bb | ||
|
|
78903fa2e5 | ||
|
|
514de64303 | ||
|
|
bad63265f8 | ||
|
|
66a9dcd2e6 | ||
|
|
df1647f1f9 | ||
|
|
89ecded480 | ||
|
|
2b35b37a66 |
@@ -3,6 +3,32 @@ Flask-Security Changelog
|
||||
|
||||
Here you can see the full list of changes between each Flask-Security release.
|
||||
|
||||
Version 1.6.8
|
||||
-------------
|
||||
|
||||
Released August 1st 2013
|
||||
|
||||
- Fixed bug with case sensitivity of email address during login
|
||||
- Code cleanup regarding token_callback
|
||||
- Ignore validation errors in find_user function for MongoEngineUserDatastore
|
||||
|
||||
Version 1.6.7
|
||||
-------------
|
||||
|
||||
Released July 11th 2013
|
||||
|
||||
- Made password length form error message configurable
|
||||
- Fixed email confirmation bug that prevented logged in users from confirming their email
|
||||
|
||||
|
||||
Version 1.6.6
|
||||
-------------
|
||||
|
||||
Released June 28th 2013
|
||||
|
||||
- Fixed dependency versions
|
||||
|
||||
|
||||
Version 1.6.5
|
||||
-------------
|
||||
|
||||
|
||||
Vendored
+2
-2
@@ -9,9 +9,9 @@
|
||||
<ul>
|
||||
<li><a href="http://pypi.python.org/pypi/Flask-Security">Flask-Security @ PyPI</a></li>
|
||||
<li><a href="http://github.com/mattupstate/flask-security">Flask-Security @ github</a></li>
|
||||
<li><a href="http://github.com/jfinkels/flask-security/issues">Issue Tracker</a></li>
|
||||
<li><a href="http://github.com/mattupstate/flask-security/issues">Issue Tracker</a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><a href="http://pypi.python.org/pypi/Flask-Social">Flask-Social</a></li>
|
||||
<li><a href="http://github.com/mattupstate/flask-social">Flask-Social @ github</a></li>
|
||||
</ul>
|
||||
</ul>
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ copyright = u'2012, Matt Wright'
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '1.6.5'
|
||||
version = '1.6.8'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = version
|
||||
|
||||
|
||||
+33
-19
@@ -56,6 +56,8 @@ URLs and Views
|
||||
``/register``.
|
||||
``SECURITY_RESET_URL`` Specifies the password reset URL. Defaults to
|
||||
``/reset``.
|
||||
``SECURITY_CHANGE_URL`` Specifies the password change URL. Defaults to
|
||||
``/change``.
|
||||
``SECURITY_CONFIRM_URL`` Specifies the email confirmation URL. Defaults
|
||||
to ``/confirm``.
|
||||
``SECURITY_POST_LOGIN_VIEW`` Specifies the default view to redirect to after
|
||||
@@ -86,8 +88,14 @@ URLs and Views
|
||||
successfully resets their password. This value
|
||||
can be set to a URL or an endpoint name. If this
|
||||
value is ``None`` the user is redirected to the
|
||||
value of ``SECURITY_POST_LOGIN_VIEW``. Defaults to
|
||||
``None``.
|
||||
value of ``SECURITY_POST_LOGIN_VIEW``. Defaults
|
||||
to ``None``.
|
||||
``SECURITY_POST_CHANGE_VIEW`` Specifies the view to redirect to after a user
|
||||
successfully changes their password. This value
|
||||
can be set to a URL or an endpoint name. If this
|
||||
value is ``None`` the user is redirected to the
|
||||
value of ``SECURITY_POST_LOGIN_VIEW``. Defaults
|
||||
to ``None``.
|
||||
``SECURITY_UNAUTHORIZED_VIEW`` Specifies the view to redirect to if a user
|
||||
attempts to access a URL/endpoint that they do
|
||||
not have permission to access. If this value is
|
||||
@@ -167,23 +175,29 @@ Email
|
||||
|
||||
.. tabularcolumns:: |p{6.5cm}|p{8.5cm}|
|
||||
|
||||
=========================================== ====================================
|
||||
``SECURITY_EMAIL_SUBJECT_REGISTER`` Sets the subject for the
|
||||
confirmation email. Defaults to
|
||||
``Welcome``
|
||||
``SECURITY_EMAIL_SUBJECT_PASSWORDLESS`` Sets the subject for the
|
||||
passwordless feature. Defaults to
|
||||
``Login instructions``
|
||||
``SECURITY_EMAIL_SUBJECT_PASSWORD_NOTICE`` Sets subject for the password
|
||||
notice. Defaults to
|
||||
``Your password has been reset``
|
||||
``SECURITY_EMAIL_SUBJECT_PASSWORD_RESET`` Sets the subject for the password
|
||||
reset. Defaults to
|
||||
``Password reset instructions``
|
||||
``SECURITY_EMAIL_SUBJECT_CONFIRM`` Sets the subject for the email
|
||||
confirmation message. Defaults to
|
||||
``Please confirm your email``
|
||||
=========================================== ====================================
|
||||
================================================= ==============================
|
||||
``SECURITY_EMAIL_SUBJECT_REGISTER`` Sets the subject for the
|
||||
confirmation email. Defaults
|
||||
to ``Welcome``
|
||||
``SECURITY_EMAIL_SUBJECT_PASSWORDLESS`` Sets the subject for the
|
||||
passwordless feature. Defaults
|
||||
to ``Login instructions``
|
||||
``SECURITY_EMAIL_SUBJECT_PASSWORD_NOTICE`` Sets subject for the password
|
||||
notice. Defaults to ``Your
|
||||
password has been reset``
|
||||
``SECURITY_EMAIL_SUBJECT_PASSWORD_RESET`` Sets the subject for the
|
||||
password reset email. Defaults
|
||||
to ``Password reset
|
||||
instructions``
|
||||
``SECURITY_EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE`` Sets the subject for the
|
||||
password change notice.
|
||||
Defaults to ``Your password
|
||||
has been changed``
|
||||
``SECURITY_EMAIL_SUBJECT_CONFIRM`` Sets the subject for the email
|
||||
confirmation message. Defaults
|
||||
to ``Please confirm your
|
||||
email``
|
||||
================================================= ==============================
|
||||
|
||||
Miscellaneous
|
||||
-------------
|
||||
|
||||
+16
-1
@@ -34,7 +34,8 @@ Password encryption is enabled with `passlib`_. Passwords are stored in plain
|
||||
text by default but you can easily configure the encryption algorithm. You
|
||||
should **always use an encryption algorithm** in your production environment.
|
||||
You may also specify to use HMAC with a configured salt value in addition to the
|
||||
algorithm chosen. Bear in mind passlib does not assume which algorithm you will choose and may require additional libraries to be installed.
|
||||
algorithm chosen. Bear in mind passlib does not assume which algorithm you will
|
||||
choose and may require additional libraries to be installed.
|
||||
|
||||
|
||||
Basic HTTP Authentication
|
||||
@@ -104,6 +105,20 @@ statistics. They include:
|
||||
* Total login count
|
||||
|
||||
|
||||
JSON/Ajax Support
|
||||
-----------------
|
||||
|
||||
Flask-Security supports JSON/Ajax requests where appropriate. Just remember that
|
||||
all endpoints require a CSRF token just like HTML views. More specifically
|
||||
JSON is supported for the following operations:
|
||||
|
||||
* Login requests
|
||||
* Registration requests
|
||||
* Change password requests
|
||||
* Confirmation requests
|
||||
* Forgot password requests
|
||||
* Passwordless login requests
|
||||
|
||||
|
||||
.. _Flask-Login: http://packages.python.org/Flask-Login/
|
||||
.. _alternative token: http://packages.python.org/Flask-Login/#alternative-tokens
|
||||
|
||||
@@ -13,6 +13,7 @@ Flask application. They include:
|
||||
7. Token based password recovery / resetting (optional)
|
||||
8. User registration (optional)
|
||||
9. Login tracking (optional)
|
||||
10. JSON/Ajax Support
|
||||
|
||||
Many of these features are made possible by integrating various Flask extensions
|
||||
and libraries. They include:
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
__version__ = '1.6.5'
|
||||
__version__ = '1.6.8'
|
||||
|
||||
from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user
|
||||
from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from flask import current_app as app, request
|
||||
from flask import current_app as app
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
from .signals import password_changed
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from flask import current_app as app, request
|
||||
from flask import current_app as app
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
from .utils import send_mail, md5, url_for_security, get_token_status,\
|
||||
|
||||
@@ -110,6 +110,7 @@ _default_messages = {
|
||||
'EMAIL_NOT_PROVIDED': ('Email not provided', 'error'),
|
||||
'INVALID_EMAIL_ADDRESS': ('Invalid email address', 'error'),
|
||||
'PASSWORD_NOT_PROVIDED': ('Password not provided', 'error'),
|
||||
'PASSWORD_INVALID_LENGTH': ('Password must be at least 6 characters', 'error'),
|
||||
'USER_DOES_NOT_EXIST': ('Specified user does not exist', 'error'),
|
||||
'INVALID_PASSWORD': ('Invalid password', 'error'),
|
||||
'PASSWORDLESS_LOGIN_SUCCESSFUL': ('You have successfuly logged in.', 'success'),
|
||||
@@ -243,7 +244,7 @@ def _context_processor():
|
||||
class RoleMixin(object):
|
||||
"""Mixin for `Role` model definitions"""
|
||||
def __eq__(self, other):
|
||||
return (self.name == other or \
|
||||
return (self.name == other or
|
||||
self.name == getattr(other, 'name', None))
|
||||
|
||||
def __ne__(self, other):
|
||||
@@ -345,10 +346,10 @@ class Security(object):
|
||||
self._state = self.init_app(app, datastore, **kwargs)
|
||||
|
||||
def init_app(self, app, datastore=None, register_blueprint=True,
|
||||
login_form=None, confirm_register_form=None,
|
||||
register_form=None, forgot_password_form=None,
|
||||
reset_password_form=None, change_password_form=None,
|
||||
send_confirmation_form=None, passwordless_login_form=None):
|
||||
login_form=None, confirm_register_form=None,
|
||||
register_form=None, forgot_password_form=None,
|
||||
reset_password_form=None, change_password_form=None,
|
||||
send_confirmation_form=None, passwordless_login_form=None):
|
||||
"""Initializes the Flask-Security extension for the specified
|
||||
application and datastore implentation.
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
|
||||
class Datastore(object):
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
@@ -81,6 +82,10 @@ class UserDatastore(object):
|
||||
kwargs['roles'] = roles
|
||||
return kwargs
|
||||
|
||||
def get_user(self, id_or_email):
|
||||
"""Returns a user matching the specified ID or email address"""
|
||||
raise NotImplementedError
|
||||
|
||||
def find_user(self, *args, **kwargs):
|
||||
"""Returns a user matching the provided parameters."""
|
||||
raise NotImplementedError
|
||||
@@ -174,6 +179,11 @@ class SQLAlchemyUserDatastore(SQLAlchemyDatastore, UserDatastore):
|
||||
SQLAlchemyDatastore.__init__(self, db)
|
||||
UserDatastore.__init__(self, user_model, role_model)
|
||||
|
||||
def get_user(self, id_or_email):
|
||||
return (self.user_model.query.get(id_or_email) or
|
||||
self.user_model.query.filter(
|
||||
self.user_model.email.ilike(id_or_email)).first())
|
||||
|
||||
def find_user(self, **kwargs):
|
||||
return self.user_model.query.filter_by(**kwargs).first()
|
||||
|
||||
@@ -189,15 +199,26 @@ class MongoEngineUserDatastore(MongoEngineDatastore, UserDatastore):
|
||||
MongoEngineDatastore.__init__(self, db)
|
||||
UserDatastore.__init__(self, user_model, role_model)
|
||||
|
||||
def get_user(self, id_or_email):
|
||||
from mongoengine import ValidationError
|
||||
try:
|
||||
return self.user_model.objects(id=id_or_email).first()
|
||||
except ValidationError:
|
||||
return self.user_model.objects(email__iexact=id_or_email).first()
|
||||
|
||||
def find_user(self, **kwargs):
|
||||
try:
|
||||
from mongoengine.queryset import Q, QCombination
|
||||
except ImportError:
|
||||
from mongoengine.queryset.visitor import Q, QCombination
|
||||
from mongoengine.errors import ValidationError
|
||||
|
||||
queries = map(lambda i: Q(**{i[0]: i[1]}), kwargs.items())
|
||||
query = QCombination(QCombination.AND, queries)
|
||||
return self.user_model.objects(query).first()
|
||||
try:
|
||||
return self.user_model.objects(query).first()
|
||||
except ValidationError:
|
||||
return None
|
||||
|
||||
def find_role(self, role):
|
||||
return self.role_model.objects(name=role).first()
|
||||
@@ -216,6 +237,17 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
|
||||
UserDatastore.__init__(self, user_model, role_model)
|
||||
self.UserRole = role_link
|
||||
|
||||
def get_user(self, id_or_email):
|
||||
try:
|
||||
return self.user_model.get(self.user_model.id == id_or_email)
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
return self.user_model.get(self.user_model.email ** id_or_email)
|
||||
except self.user_model.DoesNotExist:
|
||||
pass
|
||||
return None
|
||||
|
||||
def find_user(self, **kwargs):
|
||||
try:
|
||||
return self.user_model.filter(**kwargs).get()
|
||||
|
||||
@@ -53,21 +53,17 @@ def _check_token():
|
||||
token = request.args.get(args_key, header_token)
|
||||
if request.json:
|
||||
token = request.json.get(args_key, token)
|
||||
serializer = _security.remember_token_serializer
|
||||
|
||||
try:
|
||||
data = serializer.loads(token)
|
||||
except:
|
||||
return False
|
||||
user = _security.login_manager.token_callback(token)
|
||||
|
||||
user = _security.datastore.find_user(id=data[0])
|
||||
|
||||
if utils.md5(user.password) == data[1]:
|
||||
if user and user.is_authenticated():
|
||||
app = current_app._get_current_object()
|
||||
_request_ctx_stack.top.user = user
|
||||
identity_changed.send(app, identity=Identity(user.id))
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _check_http_auth():
|
||||
auth = request.authorization or BasicAuth(username=None, password=None)
|
||||
|
||||
+21
-21
@@ -69,6 +69,7 @@ class Length(ValidatorMixin, wtf.Length):
|
||||
email_required = Required(message='EMAIL_NOT_PROVIDED')
|
||||
email_validator = Email(message='INVALID_EMAIL_ADDRESS')
|
||||
password_required = Required(message='PASSWORD_NOT_PROVIDED')
|
||||
password_length = Length(min=6, max=128, message='PASSWORD_INVALID_LENGTH')
|
||||
|
||||
|
||||
def get_form_field_label(key):
|
||||
@@ -95,35 +96,33 @@ class Form(BaseForm):
|
||||
|
||||
|
||||
class EmailFormMixin():
|
||||
email = TextField(get_form_field_label('email'),
|
||||
validators=[email_required,
|
||||
email_validator])
|
||||
email = TextField(
|
||||
get_form_field_label('email'),
|
||||
validators=[email_required, email_validator])
|
||||
|
||||
|
||||
class UserEmailFormMixin():
|
||||
user = None
|
||||
email = TextField(get_form_field_label('email'),
|
||||
validators=[email_required,
|
||||
email_validator,
|
||||
valid_user_email])
|
||||
email = TextField(
|
||||
get_form_field_label('email'),
|
||||
validators=[email_required, email_validator, valid_user_email])
|
||||
|
||||
|
||||
class UniqueEmailFormMixin():
|
||||
email = TextField(get_form_field_label('email'),
|
||||
validators=[email_required,
|
||||
email_validator,
|
||||
unique_user_email])
|
||||
email = TextField(
|
||||
get_form_field_label('email'),
|
||||
validators=[email_required, email_validator, unique_user_email])
|
||||
|
||||
|
||||
class PasswordFormMixin():
|
||||
password = PasswordField(get_form_field_label('password'),
|
||||
validators=[password_required])
|
||||
password = PasswordField(
|
||||
get_form_field_label('password'), validators=[password_required])
|
||||
|
||||
|
||||
class NewPasswordFormMixin():
|
||||
password = PasswordField(get_form_field_label('password'),
|
||||
validators=[password_required,
|
||||
Length(min=6, max=128)])
|
||||
password = PasswordField(
|
||||
get_form_field_label('password'),
|
||||
validators=[password_required, password_length])
|
||||
|
||||
|
||||
class PasswordConfirmFormMixin():
|
||||
@@ -220,7 +219,7 @@ class LoginForm(Form, NextFormMixin):
|
||||
self.password.errors.append(get_message('PASSWORD_NOT_PROVIDED')[0])
|
||||
return False
|
||||
|
||||
self.user = _datastore.find_user(email=self.email.data)
|
||||
self.user = _datastore.get_user(self.email.data)
|
||||
|
||||
if self.user is None:
|
||||
self.email.errors.append(get_message('USER_DOES_NOT_EXIST')[0])
|
||||
@@ -255,11 +254,12 @@ class ResetPasswordForm(Form, NewPasswordFormMixin, PasswordConfirmFormMixin):
|
||||
class ChangePasswordForm(Form, PasswordFormMixin):
|
||||
"""The default change password form"""
|
||||
|
||||
new_password = PasswordField(get_form_field_label('new_password'),
|
||||
validators=[password_required,
|
||||
Length(min=6, max=128)])
|
||||
new_password = PasswordField(
|
||||
get_form_field_label('new_password'),
|
||||
validators=[password_required, password_length])
|
||||
|
||||
new_password_confirm = PasswordField(get_form_field_label('retype_password'),
|
||||
new_password_confirm = PasswordField(
|
||||
get_form_field_label('retype_password'),
|
||||
validators=[EqualTo('new_password', message='RETYPE_PASSWORD_MISMATCH')])
|
||||
|
||||
submit = SubmitField(get_form_field_label('change_password'))
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from flask import request, current_app as app
|
||||
from flask import current_app as app
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
from .signals import login_instructions_sent
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from flask import current_app as app, request
|
||||
from flask import current_app as app
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
from .signals import password_reset, reset_password_instructions_sent
|
||||
@@ -67,6 +67,7 @@ def reset_password_token_status(token):
|
||||
"""
|
||||
return get_token_status(token, 'reset', 'RESET_PASSWORD')
|
||||
|
||||
|
||||
def update_password(user, password):
|
||||
"""Update the specified user's password
|
||||
|
||||
|
||||
@@ -38,6 +38,6 @@ def register_user(**kwargs):
|
||||
|
||||
if config_value('SEND_REGISTER_EMAIL'):
|
||||
send_mail(config_value('EMAIL_SUBJECT_REGISTER'), user.email, 'welcome',
|
||||
user=user, confirmation_link=confirmation_link)
|
||||
user=user, confirmation_link=confirmation_link)
|
||||
|
||||
return user
|
||||
|
||||
@@ -78,8 +78,9 @@ def get_hmac(password):
|
||||
return password
|
||||
|
||||
if _security.password_salt is None:
|
||||
raise RuntimeError('The configuration value `SECURITY_PASSWORD_SALT` '
|
||||
'must not be None when the value of `SECURITY_PASSWORD_HASH` is '
|
||||
raise RuntimeError(
|
||||
'The configuration value `SECURITY_PASSWORD_SALT` must '
|
||||
'not be None when the value of `SECURITY_PASSWORD_HASH` is '
|
||||
'set to "%s"' % _security.password_hash)
|
||||
|
||||
h = hmac.new(_security.password_salt, password.encode('utf-8'), hashlib.sha512)
|
||||
@@ -386,5 +387,3 @@ def capture_signals():
|
||||
confirm_instructions_sent, login_instructions_sent,
|
||||
password_reset, password_changed,
|
||||
reset_password_instructions_sent])
|
||||
|
||||
|
||||
|
||||
@@ -200,7 +200,6 @@ def send_confirmation():
|
||||
**_ctx('send_confirmation'))
|
||||
|
||||
|
||||
@anonymous_user_required
|
||||
def confirm_email(token):
|
||||
"""View function which handles a email confirmation request."""
|
||||
|
||||
@@ -217,8 +216,11 @@ def confirm_email(token):
|
||||
return redirect(get_url(_security.confirm_error_view) or
|
||||
url_for('send_confirmation'))
|
||||
|
||||
if user != current_user:
|
||||
logout_user()
|
||||
login_user(user)
|
||||
|
||||
confirm_user(user)
|
||||
login_user(user)
|
||||
after_this_request(_commit)
|
||||
do_flash(*get_message('EMAIL_CONFIRMED'))
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ from setuptools import setup
|
||||
|
||||
setup(
|
||||
name='Flask-Security',
|
||||
version='1.6.5',
|
||||
version='1.6.8',
|
||||
url='https://github.com/mattupstate/flask-security',
|
||||
license='MIT',
|
||||
author='Matt Wright',
|
||||
@@ -35,10 +35,10 @@ setup(
|
||||
platforms='any',
|
||||
install_requires=[
|
||||
'Flask>=0.9',
|
||||
'Flask-Login==0.2.3',
|
||||
'Flask-Mail==0.7.3',
|
||||
'Flask-Principal==0.3.3',
|
||||
'Flask-WTF==0.8',
|
||||
'Flask-Login>=0.2.3',
|
||||
'Flask-Mail>=0.7.3',
|
||||
'Flask-Principal>=0.3.3',
|
||||
'Flask-WTF>=0.8',
|
||||
'itsdangerous>=0.17',
|
||||
'passlib>=1.6.1',
|
||||
],
|
||||
|
||||
+7
-6
@@ -30,7 +30,8 @@ class SecurityTest(TestCase):
|
||||
session['csrf'] = 'csrf_token'
|
||||
|
||||
csrf_hmac = hmac.new(self.app.config['SECRET_KEY'],
|
||||
'csrf_token'.encode('utf8'), digestmod=sha1)
|
||||
'csrf_token'.encode('utf8'),
|
||||
digestmod=sha1)
|
||||
self.csrf_token = '##' + csrf_hmac.hexdigest()
|
||||
|
||||
def _create_app(self, auth_config, **kwargs):
|
||||
@@ -38,17 +39,17 @@ class SecurityTest(TestCase):
|
||||
|
||||
def _get(self, route, content_type=None, follow_redirects=None, headers=None):
|
||||
return self.client.get(route, follow_redirects=follow_redirects,
|
||||
content_type=content_type or 'text/html',
|
||||
headers=headers)
|
||||
content_type=content_type or 'text/html',
|
||||
headers=headers)
|
||||
|
||||
def _post(self, route, data=None, content_type=None, follow_redirects=True, headers=None):
|
||||
if isinstance(data, dict):
|
||||
data['csrf_token'] = self.csrf_token
|
||||
|
||||
content_type = content_type or 'application/x-www-form-urlencoded'
|
||||
return self.client.post(route, data=data,
|
||||
follow_redirects=follow_redirects,
|
||||
content_type=content_type or 'application/x-www-form-urlencoded',
|
||||
headers=headers)
|
||||
follow_redirects=follow_redirects,
|
||||
content_type=content_type, headers=headers)
|
||||
|
||||
def register(self, email, password='password'):
|
||||
data = dict(email=email, password=password, csrf_token=self.csrf_token)
|
||||
|
||||
+41
-27
@@ -122,7 +122,6 @@ class DefaultTemplatePathTests(SecurityTest):
|
||||
'SECURITY_LOGIN_USER_TEMPLATE': 'custom_security/login_user.html',
|
||||
}
|
||||
|
||||
|
||||
def test_login_user_template(self):
|
||||
r = self._get('/login')
|
||||
|
||||
@@ -155,9 +154,8 @@ class RecoverableTemplatePathTests(SecurityTest):
|
||||
|
||||
def test_reset_password_template(self):
|
||||
with capture_reset_password_requests() as requests:
|
||||
r = self._post('/reset',
|
||||
data=dict(email='joe@lp.com'),
|
||||
follow_redirects=True)
|
||||
r = self._post('/reset', data=dict(email='joe@lp.com'),
|
||||
follow_redirects=True)
|
||||
|
||||
t = requests[0]['token']
|
||||
|
||||
@@ -336,6 +334,24 @@ class LoginWithoutImmediateConfirmTests(SecurityTest):
|
||||
r = self._post('/register', data=data, follow_redirects=True)
|
||||
self.assertIn(e, r.data)
|
||||
|
||||
def test_confirm_email_of_user_different_than_current_user(self):
|
||||
e1 = 'dude@lp.com'
|
||||
e2 = 'lady@lp.com'
|
||||
|
||||
with capture_registrations() as registrations:
|
||||
self.register(e1)
|
||||
self.register(e2)
|
||||
token1 = registrations[0]['confirm_token']
|
||||
token2 = registrations[1]['confirm_token']
|
||||
|
||||
self.client.get('/confirm/' + token1, follow_redirects=True)
|
||||
self.client.get('/logout')
|
||||
self.authenticate(email=e1)
|
||||
r = self.client.get('/confirm/' + token2, follow_redirects=True)
|
||||
msg = self.app.config['SECURITY_MSG_EMAIL_CONFIRMED'][0]
|
||||
self.assertIn(msg, r.data)
|
||||
self.assertIn('Hello %s' % e2, r.data)
|
||||
|
||||
|
||||
class RecoverableTests(SecurityTest):
|
||||
|
||||
@@ -347,9 +363,8 @@ class RecoverableTests(SecurityTest):
|
||||
|
||||
def test_reset_view(self):
|
||||
with capture_reset_password_requests() as requests:
|
||||
r = self._post('/reset',
|
||||
data=dict(email='joe@lp.com'),
|
||||
follow_redirects=True)
|
||||
r = self._post('/reset', data=dict(email='joe@lp.com'),
|
||||
follow_redirects=True)
|
||||
t = requests[0]['token']
|
||||
r = self._get('/reset/' + t)
|
||||
self.assertIn('<h1>Reset password</h1>', r.data)
|
||||
@@ -362,20 +377,18 @@ class RecoverableTests(SecurityTest):
|
||||
|
||||
def test_forgot_password_json(self):
|
||||
r = self._post('/reset', data='{"email": "matt@lp.com"}',
|
||||
content_type="application/json")
|
||||
content_type="application/json")
|
||||
self.assertEquals(r.status_code, 200)
|
||||
|
||||
def test_forgot_password_invalid_email(self):
|
||||
r = self._post('/reset',
|
||||
data=dict(email='larry@lp.com'),
|
||||
follow_redirects=True)
|
||||
r = self._post('/reset', data=dict(email='larry@lp.com'),
|
||||
follow_redirects=True)
|
||||
self.assertIn("Specified user does not exist", r.data)
|
||||
|
||||
def test_reset_password_with_valid_token(self):
|
||||
with capture_reset_password_requests() as requests:
|
||||
r = self._post('/reset',
|
||||
data=dict(email='joe@lp.com'),
|
||||
follow_redirects=True)
|
||||
r = self._post('/reset', data=dict(email='joe@lp.com'),
|
||||
follow_redirects=True)
|
||||
t = requests[0]['token']
|
||||
|
||||
r = self._post('/reset/' + t, data={
|
||||
@@ -459,16 +472,18 @@ class ChangePasswordTest(SecurityTest):
|
||||
'new_password_confirm': 'a'
|
||||
}, follow_redirects=True)
|
||||
self.assertNotIn('You successfully changed your password', r.data)
|
||||
self.assertIn('Field must be between', r.data)
|
||||
self.assertIn('Password must be at least 6 characters', r.data)
|
||||
|
||||
def test_change_password_success(self):
|
||||
data = {
|
||||
'password': 'password',
|
||||
'new_password': 'newpassword',
|
||||
'new_password_confirm': 'newpassword'
|
||||
}
|
||||
|
||||
self.authenticate()
|
||||
with self.app.extensions['mail'].record_messages() as outbox:
|
||||
r = self._post('/change', data={
|
||||
'password': 'password',
|
||||
'new_password': 'newpassword',
|
||||
'new_password_confirm': 'newpassword'
|
||||
}, follow_redirects=True)
|
||||
r = self._post('/change', data=data, follow_redirects=True)
|
||||
|
||||
self.assertIn('You successfully changed your password', r.data)
|
||||
self.assertIn('Home Page', r.data)
|
||||
@@ -486,12 +501,13 @@ class ChangePasswordPostViewTest(SecurityTest):
|
||||
}
|
||||
|
||||
def test_change_password_success(self):
|
||||
data = {
|
||||
'password': 'password',
|
||||
'new_password': 'newpassword',
|
||||
'new_password_confirm': 'newpassword'
|
||||
}
|
||||
self.authenticate()
|
||||
r = self._post('/change', data={
|
||||
'password': 'password',
|
||||
'new_password': 'newpassword',
|
||||
'new_password_confirm': 'newpassword'
|
||||
}, follow_redirects=True)
|
||||
r = self._post('/change', data=data, follow_redirects=True)
|
||||
|
||||
self.assertIn('Profile Page', r.data)
|
||||
|
||||
@@ -774,8 +790,6 @@ class ConfirmableExtendFormsTest(SecurityTest):
|
||||
r = self._get('/register', follow_redirects=True)
|
||||
self.assertIn("My Confirm Register Email Address Field", r.data)
|
||||
|
||||
|
||||
def test_send_confirmation(self):
|
||||
r = self._get('/confirm', follow_redirects=True)
|
||||
self.assertIn("My Send Confirmation Email Address Field", r.data)
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@ class DefaultSecurityTests(SecurityTest):
|
||||
r = self.authenticate()
|
||||
self.assertIn('Hello matt@lp.com', r.data)
|
||||
|
||||
def test_authenticate_case_insensitive_email(self):
|
||||
r = self.authenticate(email='MATT@lp.com')
|
||||
self.assertIn('Hello matt@lp.com', r.data)
|
||||
|
||||
def test_unprovided_username(self):
|
||||
r = self.authenticate("")
|
||||
self.assertIn(self.get_message('EMAIL_NOT_PROVIDED'), r.data)
|
||||
@@ -59,6 +63,7 @@ class DefaultSecurityTests(SecurityTest):
|
||||
self.assertIsHomePage(r.data)
|
||||
|
||||
def test_unauthorized_access(self):
|
||||
self.logout()
|
||||
r = self._get('/profile', follow_redirects=True)
|
||||
self.assertIn('<li class="info">Please log in to access this page.</li>', r.data)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user