Merge branch 'develop' of https://github.com/mattupstate/flask-security into develop

This commit is contained in:
Derek Rushing
2015-07-06 06:51:20 -05:00
29 changed files with 295 additions and 118 deletions
+10
View File
@@ -34,3 +34,13 @@ env/
*.db
*cache*
# vim
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
*.un~
Session.vim
.netrwhist
*~
.eggs/README.txt
+1
View File
@@ -36,3 +36,4 @@ Rotem Yaari
Srijan Choudhary
Tristan Escalada
Vadim Kotov
Walt Askew
+14 -19
View File
@@ -87,43 +87,38 @@ sends the following signals.
.. data:: user_registered
Sent when a user registers on the site. It is passed a dict with
the `user` and `confirm_token`, the user being logged in and the
(if so configured) the confirmation token issued.
Sent when a user registers on the site. In addition to the app (which is the
sender), it is passed `user` and `confirm_token` arguments.
.. data:: user_confirmed
Sent when a user is confirmed. It is passed `user`, which is the
user being confirmed.
Sent when a user is confirmed. In addition to the app (which is the
sender), it is passed a `user` argument.
.. data:: confirm_instructions_sent
Sent when a user requests confirmation instructions. It is passed
the `user`.
Sent when a user requests confirmation instructions. In addition to the app
(which is the sender), it is passed a `user` argument.
.. data:: login_instructions_sent
Sent when passwordless login is used and user logs in. It is passed
a dict with the `user` and `login_token`, the user being logged in
and the (if so configured) the login token issued.
Sent when passwordless login is used and user logs in. In addition to the app
(which is the sender), it is passed `user` and `login_token` arguments.
.. data:: password_reset
Sent when a user completes a password reset. It is passed the
`user`.
Sent when a user completes a password reset. In addition to the app (which is
the sender), it is passed a `user` argument.
.. data:: password_changed
Sent when a user completes a password change. It is passed the
`user`.
Sent when a user completes a password change. In addition to the app (which is
the sender), it is passed a `user` argument.
.. data:: reset_password_instructions_sent
Sent when a user requests a password reset. It is passed a dict
with the `user` and `token`, the user being logged in and
the (if so configured) the reset token issued.
Sent when a user requests a password reset. In addition to the app (which is
the sender), it is passed `user` and `token` arguments.
All signals are also passed a `app` keyword argument, which is the
current application.
.. _Flask documentation on signals: http://flask.pocoo.org/docs/signals/
+11 -3
View File
@@ -23,7 +23,10 @@ Core
passwords. Recommended values for
production systems are ``bcrypt``,
``sha512_crypt``, or ``pbkdf2_sha512``.
Defaults to ``plaintext``.
Defaults to ``plaintext``. Note:
``bcrypt>=2.0.0`` is not currently
supported. If ``bcrypt`` is preferred,
please use ``bcrypt<2.0``.
``SECURITY_PASSWORD_SALT`` Specifies the HMAC salt. This is only
used if the password hash type is set
to something other than plain text.
@@ -31,12 +34,16 @@ Core
``SECURITY_EMAIL_SENDER`` Specifies the email address to send
emails as. Defaults to
``no-reply@localhost``.
``SECURITY_TOKEN_AUTHENTICATION_KEY`` Specifies the query sting parameter to
``SECURITY_TOKEN_AUTHENTICATION_KEY`` Specifies the query string parameter to
read when using token authentication.
Defaults to ``auth_token``.
``SECURITY_TOKEN_AUTHENTICATION_HEADER`` Specifies the HTTP header to read when
using token authentication. Defaults to
``Authentication-Token``.
``SECURITY_TOKEN_MAX_AGE`` Specifies the number of seconds before
an authentication token expires.
Defaults to None, meaning the token
never expires.
``SECURITY_DEFAULT_HTTP_AUTH_REALM`` Specifies the default authentication
realm when using basic HTTP auth.
Defaults to ``Login Required``
@@ -159,7 +166,8 @@ Feature Flags
option. Defaults to ``False``.
``SECURITY_TRACKABLE`` Specifies if Flask-Security should track basic user
login statistics. If set to ``True``, ensure your
models have the required fields/attribues. Defaults to
models have the required fields/attribues. Be sure to
use `ProxyFix <http://flask.pocoo.org/docs/0.10/deploying/wsgi-standalone/#proxy-setups>` if you are using a proxy. Defaults to
``False``
``SECURITY_PASSWORDLESS`` Specifies if Flask-Security should enable the
passwordless login feature. If set to ``True``, users
+2 -2
View File
@@ -73,8 +73,8 @@ register form or override validators::
from flask_security.forms import RegisterForm
class ExtendedRegisterForm(RegisterForm):
first_name = TextField('First Name', [Required()])
last_name = TextField('Last Name', [Required()])
first_name = StringField('First Name', [Required()])
last_name = StringField('Last Name', [Required()])
security = Security(app, user_datastore,
register_form=ExtendedRegisterForm)
+4 -4
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security
~~~~~~~~~~~~~~~~~~
flask_security
~~~~~~~~~~~~~~
Flask-Security is a Flask extension that aims to add quick and simple
security via Flask-Login, Flask-Principal, Flask-WTF, and passlib.
@@ -10,8 +10,6 @@
:license: MIT, see LICENSE for more details.
"""
__version__ = '1.7.4'
from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user
from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore
from .decorators import auth_token_required, http_auth_required, \
@@ -21,3 +19,5 @@ from .forms import ForgotPasswordForm, LoginForm, RegisterForm, \
from .signals import confirm_instructions_sent, password_reset, \
reset_password_instructions_sent, user_confirmed, user_registered
from .utils import login_user, logout_user, url_for_security
__version__ = '1.7.4'
+4 -3
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.changeable
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
flask_security.changeable
~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security recoverable module
@@ -42,4 +42,5 @@ def change_user_password(user, password):
user.password = encrypt_password(password)
_datastore.put(user)
send_password_changed_notice(user)
password_changed.send(app._get_current_object(), user=user)
password_changed.send(app._get_current_object(),
user=user._get_current_object())
+2 -2
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.confirmable
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
flask_security.confirmable
~~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security confirmable module
+21 -14
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.core
~~~~~~~~~~~~~~~~~~~~~~~
flask_security.core
~~~~~~~~~~~~~~~~~~~
Flask-Security core module
@@ -10,9 +10,9 @@
"""
from flask import current_app, render_template
from flask.ext.login import AnonymousUserMixin, UserMixin as BaseUserMixin, \
from flask_login import AnonymousUserMixin, UserMixin as BaseUserMixin, \
LoginManager, current_user
from flask.ext.principal import Principal, RoleNeed, UserNeed, Identity, \
from flask_principal import Principal, RoleNeed, UserNeed, Identity, \
identity_loaded
from itsdangerous import URLSafeTimedSerializer
from passlib.context import CryptContext
@@ -75,6 +75,7 @@ _default_config = {
'EMAIL_SENDER': 'no-reply@localhost',
'TOKEN_AUTHENTICATION_KEY': 'auth_token',
'TOKEN_AUTHENTICATION_HEADER': 'Authentication-Token',
'TOKEN_MAX_AGE': None,
'CONFIRM_SALT': 'confirm-salt',
'RESET_SALT': 'reset-salt',
'LOGIN_SALT': 'login-salt',
@@ -192,17 +193,17 @@ def _user_loader(user_id):
def _token_loader(token):
try:
data = _security.remember_token_serializer.loads(token)
data = _security.remember_token_serializer.loads(token, max_age=_security.token_max_age)
user = _security.datastore.find_user(id=data[0])
if user and safe_str_cmp(md5(user.password), data[1]):
return user
except:
pass
return AnonymousUser()
return _security.login_manager.anonymous_user()
def _identity_loader():
if not isinstance(current_user._get_current_object(), AnonymousUser):
if not isinstance(current_user._get_current_object(), AnonymousUserMixin):
identity = Identity(current_user.id)
return identity
@@ -217,9 +218,9 @@ def _on_identity_loaded(sender, identity):
identity.user = current_user
def _get_login_manager(app):
def _get_login_manager(app, anonymous_user):
lm = LoginManager()
lm.anonymous_user = AnonymousUser
lm.anonymous_user = anonymous_user or AnonymousUser
lm.login_view = '%s.login' % cv('BLUEPRINT_NAME', app=app)
lm.user_loader(_user_loader)
lm.token_loader(_token_loader)
@@ -257,14 +258,14 @@ def _get_serializer(app, name):
return URLSafeTimedSerializer(secret_key=secret_key, salt=salt)
def _get_state(app, datastore, **kwargs):
def _get_state(app, datastore, anonymous_user=None, **kwargs):
for key, value in get_config(app).items():
kwargs[key.lower()] = value
kwargs.update(dict(
app=app,
datastore=datastore,
login_manager=_get_login_manager(app),
login_manager=_get_login_manager(app, anonymous_user),
principal=_get_principal(app),
pwd_context=_get_pwd_context(app),
remember_token_serializer=_get_serializer(app, 'remember'),
@@ -272,7 +273,8 @@ def _get_state(app, datastore, **kwargs):
reset_serializer=_get_serializer(app, 'reset'),
confirm_serializer=_get_serializer(app, 'confirm'),
_context_processors={},
_send_mail_task=None
_send_mail_task=None,
_unauthorized_callback=None
))
for key, value in _default_forms.items():
@@ -380,6 +382,9 @@ class _SecurityState(object):
def send_mail_task(self, fn):
self._send_mail_task = fn
def unauthorized_handler(self, fn):
self._unauthorized_callback = fn
class Security(object):
"""The :class:`Security` class initializes the Flask-Security extension.
@@ -398,7 +403,8 @@ class Security(object):
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):
send_confirmation_form=None, passwordless_login_form=None,
anonymous_user=None):
"""Initializes the Flask-Security extension for the specified
application and datastore implentation.
@@ -424,7 +430,8 @@ class Security(object):
reset_password_form=reset_password_form,
change_password_form=change_password_form,
send_confirmation_form=send_confirmation_form,
passwordless_login_form=passwordless_login_form)
passwordless_login_form=passwordless_login_form,
anonymous_user=anonymous_user)
if register_blueprint:
app.register_blueprint(create_blueprint(state, __name__))
+3 -3
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.datastore
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
flask_security.datastore
~~~~~~~~~~~~~~~~~~~~~~~~
This module contains an user datastore classes.
@@ -195,7 +195,7 @@ class SQLAlchemyUserDatastore(SQLAlchemyDatastore, UserDatastore):
def _is_numeric(self, value):
try:
int(value)
except ValueError:
except (TypeError, ValueError):
return False
return True
+32 -13
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.decorators
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
flask_security.decorators
~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security decorators module
@@ -13,8 +13,8 @@ from collections import namedtuple
from functools import wraps
from flask import current_app, Response, request, redirect, _request_ctx_stack
from flask.ext.login import current_user, login_required # pragma: no flakes
from flask.ext.principal import RoleNeed, Permission, Identity, identity_changed
from flask_login import current_user, login_required # pragma: no flakes
from flask_principal import RoleNeed, Permission, Identity, identity_changed
from werkzeug.local import LocalProxy
from . import utils
@@ -91,9 +91,12 @@ def http_auth_required(realm):
def wrapper(*args, **kwargs):
if _check_http_auth():
return fn(*args, **kwargs)
r = _security.default_http_auth_realm if callable(realm) else realm
h = {'WWW-Authenticate': 'Basic realm="%s"' % r}
return _get_unauthorized_response(headers=h)
if _security._unauthorized_callback:
return _security._unauthorized_callback()
else:
r = _security.default_http_auth_realm if callable(realm) else realm
h = {'WWW-Authenticate': 'Basic realm="%s"' % r}
return _get_unauthorized_response(headers=h)
return wrapper
if callable(realm):
@@ -113,7 +116,10 @@ def auth_token_required(fn):
def decorated(*args, **kwargs):
if _check_token():
return fn(*args, **kwargs)
return _get_unauthorized_response()
if _security._unauthorized_callback:
return _security._unauthorized_callback()
else:
return _get_unauthorized_response()
return decorated
@@ -138,11 +144,18 @@ def auth_required(*auth_methods):
def wrapper(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
mechanisms = [login_mechanisms.get(method) for method in auth_methods]
for mechanism in mechanisms:
h = {}
mechanisms = [(method, login_mechanisms.get(method)) for method in auth_methods]
for method, mechanism in mechanisms:
if mechanism and mechanism():
return fn(*args, **kwargs)
return _get_unauthorized_response()
elif method == 'basic':
r = _security.default_http_auth_realm
h['WWW-Authenticate'] = 'Basic realm="%s"' % r
if _security._unauthorized_callback:
return _security._unauthorized_callback()
else:
return _get_unauthorized_response(headers=h)
return decorated_view
return wrapper
@@ -167,7 +180,10 @@ def roles_required(*roles):
perms = [Permission(RoleNeed(role)) for role in roles]
for perm in perms:
if not perm.can():
return _get_unauthorized_view()
if _security._unauthorized_callback:
return _security._unauthorized_callback()
else:
return _get_unauthorized_view()
return fn(*args, **kwargs)
return decorated_view
return wrapper
@@ -193,7 +209,10 @@ def roles_accepted(*roles):
perm = Permission(*[RoleNeed(role) for role in roles])
if perm.can():
return fn(*args, **kwargs)
return _get_unauthorized_view()
if _security._unauthorized_callback:
return _security._unauthorized_callback()
else:
return _get_unauthorized_view()
return decorated_view
return wrapper
+7 -7
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.forms
~~~~~~~~~~~~~~~~~~~~~~~~
flask_security.forms
~~~~~~~~~~~~~~~~~~~~
Flask-Security forms module
@@ -13,7 +13,7 @@ import inspect
from flask import request, current_app, flash
from flask_wtf import Form as BaseForm
from wtforms import TextField, PasswordField, validators, \
from wtforms import StringField, PasswordField, validators, \
SubmitField, HiddenField, BooleanField, ValidationError, Field
from flask_login import current_user
from werkzeug.local import LocalProxy
@@ -94,20 +94,20 @@ class Form(BaseForm):
class EmailFormMixin():
email = TextField(
email = StringField(
get_form_field_label('email'),
validators=[email_required, email_validator])
class UserEmailFormMixin():
user = None
email = TextField(
email = StringField(
get_form_field_label('email'),
validators=[email_required, email_validator, valid_user_email])
class UniqueEmailFormMixin():
email = TextField(
email = StringField(
get_form_field_label('email'),
validators=[email_required, email_validator, unique_user_email])
@@ -204,7 +204,7 @@ class PasswordlessLoginForm(Form, UserEmailFormMixin):
class LoginForm(Form, NextFormMixin):
"""The default login form"""
email = TextField(get_form_field_label('email'))
email = StringField(get_form_field_label('email'))
password = PasswordField(get_form_field_label('password'))
remember = BooleanField(get_form_field_label('remember_me'))
submit = SubmitField(get_form_field_label('login'))
+3 -4
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.passwordless
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
flask_security.passwordless
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security passwordless module
@@ -35,8 +35,7 @@ def send_login_instructions(user):
send_mail(config_value('EMAIL_SUBJECT_PASSWORDLESS'), user.email,
'login_instructions', user=user, login_link=login_link)
login_instructions_sent.send(app._get_current_object(),
user=user, login_token=token)
login_instructions_sent.send(app._get_current_object(), user=user, login_token=token)
def generate_login_token(user):
+16 -7
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.recoverable
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
flask_security.recoverable
~~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security recoverable module
@@ -11,6 +11,7 @@
from flask import current_app as app
from werkzeug.local import LocalProxy
from werkzeug.security import safe_str_cmp
from .signals import password_reset, reset_password_instructions_sent
from .utils import send_mail, md5, encrypt_password, url_for_security, \
@@ -35,8 +36,7 @@ def send_reset_password_instructions(user):
'reset_instructions',
user=user, reset_link=reset_link)
reset_password_instructions_sent.send(app._get_current_object(),
user=user, token=token)
reset_password_instructions_sent.send(app._get_current_object(), user=user, token=token)
def send_password_reset_notice(user):
@@ -54,7 +54,8 @@ def generate_reset_password_token(user):
:param user: The user to work with
"""
data = [str(user.id), md5(user.password)]
password_hash = md5(user.password) if user.password else None
data = [str(user.id), password_hash]
return _security.reset_serializer.dumps(data)
@@ -62,11 +63,19 @@ def reset_password_token_status(token):
"""Returns the expired status, invalid status, and user of a password reset
token. For example::
expired, invalid, user = reset_password_token_status('...')
expired, invalid, user, data = reset_password_token_status('...')
:param token: The password reset token
"""
return get_token_status(token, 'reset', 'RESET_PASSWORD')
expired, invalid, user, data = get_token_status(token, 'reset', 'RESET_PASSWORD',
return_data=True)
if not invalid:
if user.password:
password_hash = md5(user.password)
if not safe_str_cmp(password_hash, data[1]):
invalid = True
return expired, invalid, user
def update_password(user, password):
+2 -2
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.registerable
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
flask_security.registerable
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security registerable module
+5 -5
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.script
~~~~~~~~~~~~~~~~~~~~~~~~~
flask_security.script
~~~~~~~~~~~~~~~~~~~~~
Flask-Security script module
@@ -18,7 +18,7 @@ except ImportError:
import re
from flask import current_app
from flask.ext.script import Command, Option
from flask_script import Command, Option
from werkzeug.local import LocalProxy
from .utils import encrypt_password
@@ -100,7 +100,7 @@ class AddRoleCommand(_RoleCommand):
class RemoveRoleCommand(_RoleCommand):
"""Add a role to a user"""
"""Remove a role from a user"""
@commit
def run(self, user_identifier, role_name):
@@ -124,7 +124,7 @@ class DeactivateUserCommand(_ToggleActiveCommand):
class ActivateUserCommand(_ToggleActiveCommand):
"""Deactive a user"""
"""Activate a user"""
@commit
def run(self, user_identifier):
+2 -2
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.signals
~~~~~~~~~~~~~~~~~~~~~~~~~~
flask_security.signals
~~~~~~~~~~~~~~~~~~~~~~
Flask-Security signals module
+22 -10
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.utils
~~~~~~~~~~~~~~~~~~~~~~~~
flask_security.utils
~~~~~~~~~~~~~~~~~~~~
Flask-Security utils module
@@ -23,9 +23,9 @@ from contextlib import contextmanager
from datetime import datetime, timedelta
from flask import url_for, flash, current_app, request, session, render_template
from flask.ext.login import login_user as _login_user, logout_user as _logout_user
from flask.ext.mail import Message
from flask.ext.principal import Identity, AnonymousIdentity, identity_changed
from flask_login import login_user as _login_user, logout_user as _logout_user
from flask_mail import Message
from flask_principal import Identity, AnonymousIdentity, identity_changed
from itsdangerous import BadSignature, SignatureExpired
from werkzeug.local import LocalProxy
@@ -62,10 +62,10 @@ def login_user(user, remember=None):
return False
if _security.trackable:
if 'X-Forwarded-For' not in request.headers:
remote_addr = request.remote_addr or 'untrackable'
if 'X-Forwarded-For' in request.headers:
remote_addr = request.headers.getlist("X-Forwarded-For")[0].rpartition(' ')[-1]
else:
remote_addr = request.headers.getlist("X-Forwarded-For")[0]
remote_addr = request.remote_addr or 'untrackable'
old_current_login, new_current_login = user.current_login_at, datetime.utcnow()
old_current_ip, new_current_ip = user.current_login_ip, remote_addr
@@ -188,6 +188,14 @@ def get_url(endpoint_or_url):
return endpoint_or_url
def slash_url_suffix(url, suffix):
"""Adds a slash either to the beginning or the end of a suffix
(which is to be appended to a URL), depending on whether or not
the URL ends with a slash."""
return url.endswith('/') and ('%s/' % suffix) or ('/%s' % suffix)
def get_security_endpoint_name(endpoint):
return '%s.%s' % (_security.blueprint_name, endpoint)
@@ -333,7 +341,7 @@ def send_mail(subject, recipient, template, **context):
mail.send(msg)
def get_token_status(token, serializer, max_age=None):
def get_token_status(token, serializer, max_age=None, return_data=False):
"""Get the status of a token.
:param token: The token to check
@@ -359,7 +367,11 @@ def get_token_status(token, serializer, max_age=None):
user = _datastore.find_user(id=data[0])
expired = expired and (user is not None)
return expired, invalid, user
if return_data:
return expired, invalid, user, data
else:
return expired, invalid, user
def get_identity_attributes(app=None):
+7 -6
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.views
~~~~~~~~~~~~~~~~~~~~~~~~
flask_security.views
~~~~~~~~~~~~~~~~~~~~
Flask-Security views module
@@ -26,7 +26,7 @@ from .changeable import change_user_password
from .registerable import register_user
from .utils import config_value, do_flash, get_url, get_post_login_redirect, \
get_post_register_redirect, get_message, login_user, logout_user, \
url_for_security as url_for
url_for_security as url_for, slash_url_suffix
# Convenient references
_security = LocalProxy(lambda: current_app.extensions['security'])
@@ -238,6 +238,7 @@ def confirm_email(token):
get_url(_security.post_login_view))
@anonymous_user_required
def forgot_password():
"""View function that handles a forgotten password request."""
@@ -333,7 +334,7 @@ def create_blueprint(state, import_name):
bp.route(state.login_url,
methods=['GET', 'POST'],
endpoint='login')(send_login)
bp.route(state.login_url + '/<token>',
bp.route(state.login_url + slash_url_suffix(state.login_url, '<token>'),
endpoint='token_login')(token_login)
else:
bp.route(state.login_url,
@@ -349,7 +350,7 @@ def create_blueprint(state, import_name):
bp.route(state.reset_url,
methods=['GET', 'POST'],
endpoint='forgot_password')(forgot_password)
bp.route(state.reset_url + '/<token>',
bp.route(state.reset_url + slash_url_suffix(state.reset_url, '<token>'),
methods=['GET', 'POST'],
endpoint='reset_password')(reset_password)
@@ -362,7 +363,7 @@ def create_blueprint(state, import_name):
bp.route(state.confirm_url,
methods=['GET', 'POST'],
endpoint='send_confirmation')(send_confirmation)
bp.route(state.confirm_url + '/<token>',
bp.route(state.confirm_url + slash_url_suffix(state.confirm_url, '<token>'),
methods=['GET', 'POST'],
endpoint='confirm_email')(confirm_email)
+2 -1
View File
@@ -1,7 +1,8 @@
Flask-SQLAlchemy>=1.0
bcrypt>=1.0.2
bcrypt>=1.0.2,<2.0.0
flask-mongoengine>=0.7.0
flask-peewee>=0.6.5
pymongo==2.8
pytest>=2.5.2
pytest-cache>=1.0
pytest-cov>=1.6
+4
View File
@@ -8,6 +8,8 @@
import pytest
from flask import Flask
from flask_security.core import UserMixin
from flask_security.signals import password_changed
from utils import authenticate
@@ -20,6 +22,8 @@ def test_recoverable_flag(app, client, get_message):
@password_changed.connect_via(app)
def on_password_changed(app, user):
assert isinstance(app, Flask)
assert isinstance(user, UserMixin)
recorded.append(user)
authenticate(client)
+13
View File
@@ -226,6 +226,19 @@ def test_multi_auth_basic(client):
assert response.status_code == 401
def test_multi_auth_basic_invalid(client):
response = client.get('/multi_auth', headers={
'Authorization': 'Basic %s' % base64.b64encode(b"bogus:bogus").decode('utf-8')
})
assert b'<h1>Unauthorized</h1>' in response.data
assert 'WWW-Authenticate' in response.headers
assert 'Basic realm="Login Required"' == response.headers['WWW-Authenticate']
response = client.get('/multi_auth')
print(response.headers)
assert response.status_code == 401
def test_multi_auth_token(client):
response = json_authenticate(client)
token = response.jdata['response']['user']['authentication_token']
+6
View File
@@ -10,6 +10,8 @@ import time
import pytest
from flask import Flask
from flask_security.core import UserMixin
from flask_security.signals import user_confirmed, confirm_instructions_sent
from flask_security.utils import capture_registrations
@@ -25,10 +27,14 @@ def test_confirmable_flag(app, client, sqlalchemy_datastore, get_message):
@user_confirmed.connect_via(app)
def on_confirmed(app, user):
assert isinstance(app, Flask)
assert isinstance(user, UserMixin)
recorded_confirms.append(user)
@confirm_instructions_sent.connect_via(app)
def on_instructions_sent(app, user):
assert isinstance(app, Flask)
assert isinstance(user, UserMixin)
recorded_instructions_sent.append(user)
# Test login before confirmation
+2
View File
@@ -81,6 +81,8 @@ def test_context_processors(client, app):
def mail():
return {'foo': 'bar'}
client.get('/logout')
with app.mail.record_messages() as outbox:
client.post('/reset', data=dict(email='matt@lp.com'))
+26 -9
View File
@@ -11,7 +11,8 @@ import pytest
from flask_security import Security
from flask_security.forms import LoginForm, RegisterForm, ConfirmRegisterForm, \
SendConfirmationForm, PasswordlessLoginForm, ForgotPasswordForm, ResetPasswordForm, \
ChangePasswordForm, TextField, PasswordField, email_required, email_validator, valid_user_email
ChangePasswordForm, StringField, PasswordField, email_required, email_validator, \
valid_user_email
from flask_security.utils import capture_reset_password_requests, md5, string_types
from utils import authenticate, init_app_with_options, populate_data
@@ -41,17 +42,17 @@ def test_register_blueprint_flag(app, sqlalchemy_datastore):
@pytest.mark.changeable()
def test_basic_custom_forms(app, sqlalchemy_datastore):
class MyLoginForm(LoginForm):
email = TextField('My Login Email Address Field')
email = StringField('My Login Email Address Field')
class MyRegisterForm(RegisterForm):
email = TextField('My Register Email Address Field')
email = StringField('My Register Email Address Field')
class MyForgotPasswordForm(ForgotPasswordForm):
email = TextField('My Forgot Email Address Field',
validators=[email_required, email_validator, valid_user_email])
email = StringField('My Forgot Email Address Field',
validators=[email_required, email_validator, valid_user_email])
class MyResetPasswordForm(ResetPasswordForm):
password = TextField('My Reset Password Field')
password = StringField('My Reset Password Field')
class MyChangePasswordForm(ChangePasswordForm):
password = PasswordField('My Change Password Field')
@@ -96,10 +97,10 @@ def test_confirmable_custom_form(app, sqlalchemy_datastore):
app.config['SECURITY_CONFIRMABLE'] = True
class MyRegisterForm(ConfirmRegisterForm):
email = TextField('My Register Email Address Field')
email = StringField('My Register Email Address Field')
class MySendConfirmationForm(SendConfirmationForm):
email = TextField('My Send Confirmation Email Address Field')
email = StringField('My Send Confirmation Email Address Field')
app.security = Security(app,
datastore=sqlalchemy_datastore,
@@ -119,7 +120,7 @@ def test_passwordless_custom_form(app, sqlalchemy_datastore):
app.config['SECURITY_PASSWORDLESS'] = True
class MyPasswordlessLoginForm(PasswordlessLoginForm):
email = TextField('My Passwordless Email Address Field')
email = StringField('My Passwordless Email Address Field')
app.security = Security(app,
datastore=sqlalchemy_datastore,
@@ -191,3 +192,19 @@ def test_password_unicode_password_salt(client):
assert response.status_code == 302
response = authenticate(client, follow_redirects=True)
assert b'Hello matt@lp.com' in response.data
def test_set_unauthorized_handler(app, client):
@app.security.unauthorized_handler
def unauthorized():
app.unauthorized_handler_set = True
return 'unauthorized-handler-set', 401
app.unauthorized_handler_set = False
authenticate(client, "joe@lp.com")
response = client.get("/admin", follow_redirects=True)
assert app.unauthorized_handler_set is True
assert b'unauthorized-handler-set' in response.data
assert response.status_code == 401
+6 -1
View File
@@ -10,8 +10,10 @@ import time
import pytest
from flask import Flask
from flask_security.core import UserMixin
from flask_security.signals import login_instructions_sent
from flask_security.utils import capture_passwordless_login_requests
from flask_security.utils import capture_passwordless_login_requests, string_types
from utils import logout
@@ -23,6 +25,9 @@ def test_trackable_flag(app, client, get_message):
@login_instructions_sent.connect_via(app)
def on_instructions_sent(app, user, login_token):
assert isinstance(app, Flask)
assert isinstance(user, UserMixin)
assert isinstance(login_token, string_types)
recorded.append(user)
# Test disabled account
+47 -1
View File
@@ -10,8 +10,10 @@ import time
import pytest
from flask import Flask
from flask_security.core import UserMixin
from flask_security.signals import reset_password_instructions_sent, password_reset
from flask_security.utils import capture_reset_password_requests
from flask_security.utils import capture_reset_password_requests, string_types
from utils import authenticate, logout
@@ -28,6 +30,9 @@ def test_recoverable_flag(app, client, get_message):
@reset_password_instructions_sent.connect_via(app)
def on_instructions_sent(app, user, token):
assert isinstance(app, Flask)
assert isinstance(user, UserMixin)
assert isinstance(token, string_types)
recorded_instructions_sent.append(user)
# Test the reset view
@@ -117,6 +122,47 @@ def test_expired_reset_token(client, get_message):
assert msg in response.data
def test_used_reset_token(client, get_message):
with capture_reset_password_requests() as requests:
client.post('/reset', data=dict(email='joe@lp.com'), follow_redirects=True)
token = requests[0]['token']
# use the token
response = client.post('/reset/' + token, data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
assert get_message('PASSWORD_RESET') in response.data
logout(client)
# attempt to use it a second time
response2 = client.post('/reset/' + token, data={
'password': 'otherpassword',
'password_confirm': 'otherpassword'
}, follow_redirects=True)
msg = get_message('INVALID_RESET_PASSWORD_TOKEN')
assert msg in response2.data
def test_reset_passwordless_user(client, get_message):
with capture_reset_password_requests() as requests:
client.post('/reset', data=dict(email='jess@lp.com'), follow_redirects=True)
token = requests[0]['token']
# use the token
response = client.post('/reset/' + token, data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
assert get_message('PASSWORD_RESET') in response.data
@pytest.mark.settings(reset_url='/custom_reset')
def test_custom_reset_url(client):
response = client.get('/custom_reset')
+5
View File
@@ -8,6 +8,8 @@
import pytest
from flask import Flask
from flask_security.core import UserMixin
from flask_security.signals import user_registered
from utils import authenticate, logout
@@ -26,6 +28,9 @@ def test_registerable_flag(client, app, get_message):
# Test registering is successful, sends email, and fires signal
@user_registered.connect_via(app)
def on_user_registerd(app, user, confirm_token):
assert isinstance(app, Flask)
assert isinstance(user, UserMixin)
assert confirm_token is None
recorded.append(user)
data = dict(
+16
View File
@@ -26,3 +26,19 @@ def test_trackable_flag(app, client):
assert user.last_login_ip == 'untrackable'
assert user.current_login_ip == '127.0.0.1'
assert user.login_count == 2
def test_trackable_with_multiple_ips_in_headers(app, client):
e = 'matt@lp.com'
authenticate(client, email=e)
logout(client)
authenticate(client, email=e, headers={
'X-Forwarded-For': '99.99.99.99, 88.88.88.88'})
with app.app_context():
user = app.security.datastore.find_user(email=e)
assert user.last_login_at is not None
assert user.current_login_at is not None
assert user.last_login_ip == 'untrackable'
assert user.current_login_ip == '88.88.88.88'
assert user.login_count == 2