mirror of
https://github.com/wassname/flask-security.git
synced 2026-08-07 11:22:21 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7d0ea9cce | ||
|
|
94c7c09dc2 | ||
|
|
f6405797f1 | ||
|
|
2cffb6634d | ||
|
|
824a52b883 | ||
|
|
fe7e4c4afc | ||
|
|
591bc27a5e | ||
|
|
5bc37add88 | ||
|
|
8b036f2a3e | ||
|
|
76ad77a233 | ||
|
|
679cee7969 | ||
|
|
3d7b97ac31 | ||
|
|
3a0af73231 | ||
|
|
3458391791 | ||
|
|
37908ca335 | ||
|
|
f83cd56ff2 | ||
|
|
c10ec29fe5 | ||
|
|
d2fe7aefcb | ||
|
|
708ddeb6c4 | ||
|
|
2aeee348d4 | ||
|
|
52b177cd2e | ||
|
|
dab2fc8c8b | ||
|
|
31e3ab5470 | ||
|
|
6bdcd2930d | ||
|
|
1076887900 | ||
|
|
285fe888da | ||
|
|
bf139847cb | ||
|
|
15c9ef1d07 | ||
|
|
d1ec38ea18 | ||
|
|
577a3a8110 | ||
|
|
7b474efd7a | ||
|
|
f12bc17e7e | ||
|
|
999f882f61 | ||
|
|
a140c01b57 | ||
|
|
76cf3eaf6a | ||
|
|
a6b5d3053c | ||
|
|
0a48997fdd | ||
|
|
96f1b3e0d1 | ||
|
|
0facdaacd9 | ||
|
|
9cadf855a4 |
@@ -4,6 +4,7 @@ python:
|
||||
- "2.6"
|
||||
- "2.7"
|
||||
- "3.3"
|
||||
- "3.4"
|
||||
- "pypy"
|
||||
|
||||
install:
|
||||
|
||||
@@ -3,6 +3,28 @@ Flask-Security Changelog
|
||||
|
||||
Here you can see the full list of changes between each Flask-Security release.
|
||||
|
||||
Version 1.7.4
|
||||
-------------
|
||||
|
||||
Released October 13th 2014
|
||||
|
||||
- Fixed a bug related to changing existing passwords from plaintext to hashed
|
||||
- Fixed a bug in form validation that did not enforce case insensivitiy
|
||||
- Fixed a bug with validating redirects
|
||||
|
||||
|
||||
Version 1.7.3
|
||||
-------------
|
||||
|
||||
Released June 10th 2014
|
||||
|
||||
- Fixed a bug where redirection to `SECURITY_POST_LOGIN_VIEW` was not respected
|
||||
- Fixed string encoding in various places to be friendly to unicode
|
||||
- Now using `werkzeug.security.safe_str_cmp` to check tokens
|
||||
- Removed user information from JSON output on `/reset` responses
|
||||
- Added Python 3.4 support
|
||||
|
||||
|
||||
Version 1.7.2
|
||||
-------------
|
||||
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ copyright = u'2012, Matt Wright'
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '1.7.2'
|
||||
version = '1.7.4'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = version
|
||||
|
||||
|
||||
+10
-10
@@ -15,7 +15,7 @@ Core
|
||||
``SECURITY_URL_PREFIX`` Specifies the URL prefix for the
|
||||
Flask-Security blueprint. Defaults to
|
||||
``None``.
|
||||
``SECURITY_FLASH_MESSAGES`` Specifies wether or not to flash
|
||||
``SECURITY_FLASH_MESSAGES`` Specifies whether or not to flash
|
||||
messages during security procedures.
|
||||
Defaults to ``True``.
|
||||
``SECURITY_PASSWORD_HASH`` Specifies the password hash algorithm to
|
||||
@@ -69,37 +69,37 @@ URLs and Views
|
||||
``SECURITY_CONFIRM_ERROR_VIEW`` Specifies the view to redirect to if a
|
||||
confirmation error occurs. This value can be set
|
||||
to a URL or an endpoint name. If this value is
|
||||
``None`` the user is presented the default view
|
||||
``None``, the user is presented the default view
|
||||
to resend a confirmation link. Defaults to
|
||||
``None``.
|
||||
``SECURITY_POST_REGISTER_VIEW`` Specifies the view to redirect to after a user
|
||||
successfully registers. 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
|
||||
``None``, the user is redirected to the value of
|
||||
``SECURITY_POST_LOGIN_VIEW``. Defaults to
|
||||
``None``.
|
||||
``SECURITY_POST_CONFIRM_VIEW`` Specifies the view to redirect to after a user
|
||||
successfully confirms their email. This value
|
||||
can be set to a URL or an endpoint name. If this
|
||||
value is ``None`` the user is redirected to the
|
||||
value is ``None``, the user is redirected to the
|
||||
value of ``SECURITY_POST_LOGIN_VIEW``. Defaults
|
||||
to ``None``.
|
||||
``SECURITY_POST_RESET_VIEW`` Specifies the view to redirect to after a user
|
||||
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 is ``None``, the user is redirected to the
|
||||
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 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
|
||||
``None`` the user is presented with a default
|
||||
``None``, the user is presented with a default
|
||||
HTTP 403 response. Defaults to ``None``.
|
||||
=============================== ================================================
|
||||
|
||||
@@ -144,7 +144,7 @@ Feature Flags
|
||||
========================= ======================================================
|
||||
``SECURITY_CONFIRMABLE`` Specifies if users are required to confirm their email
|
||||
address when registering a new account. If this value
|
||||
is `True` Flask-Security creates an endpoint to handle
|
||||
is `True`, Flask-Security creates an endpoint to handle
|
||||
confirmations and requests to resend confirmation
|
||||
instructions. The URL for this endpoint is specified
|
||||
by the ``SECURITY_CONFIRM_URL`` configuration option.
|
||||
@@ -158,11 +158,11 @@ Feature Flags
|
||||
specified by the ``SECURITY_RESET_URL`` configuration
|
||||
option. Defaults to ``False``.
|
||||
``SECURITY_TRACKABLE`` Specifies if Flask-Security should track basic user
|
||||
login statistics. If set to ``True`` ensure your
|
||||
login statistics. If set to ``True``, ensure your
|
||||
models have the required fields/attribues. Defaults to
|
||||
``False``
|
||||
``SECURITY_PASSWORDLESS`` Specifies if Flask-Security should enable the
|
||||
passwordless login feature. If set to ``True`` users
|
||||
passwordless login feature. If set to ``True``, users
|
||||
are not required to enter a password to login but are
|
||||
sent an email with a login link. This feature is
|
||||
experimental and should be used with caution. Defaults
|
||||
|
||||
@@ -2,7 +2,7 @@ Customizing Views
|
||||
=================
|
||||
|
||||
Flask-Security bootstraps your application with various views for handling its
|
||||
configured features to get you up and running as quick as possible. However,
|
||||
configured features to get you up and running as quickly as possible. However,
|
||||
you'll probably want to change the way these views look to be more in line with
|
||||
your application's visual design.
|
||||
|
||||
@@ -36,7 +36,7 @@ Flask application context processor:
|
||||
* ``<template_name>_form``: A form object for the view
|
||||
* ``security``: The Flask-Security extension object
|
||||
|
||||
To add more values to the template context you can specify a context processor
|
||||
To add more values to the template context, you can specify a context processor
|
||||
for all views or a specific view. For example::
|
||||
|
||||
security = Security(app, user_datastore)
|
||||
@@ -106,7 +106,7 @@ The following is a list of all the available form overrides:
|
||||
Emails
|
||||
------
|
||||
|
||||
Flask-Security is also packaged with a default tempalte for each email that it
|
||||
Flask-Security is also packaged with a default template for each email that it
|
||||
may send. Templates are located within the subfolder named ``security/email``.
|
||||
The following is a list of email templates:
|
||||
|
||||
@@ -131,7 +131,7 @@ Overriding these templates is simple:
|
||||
|
||||
Each template is passed a template context object that includes values for any
|
||||
links that are required in the email. If you require more values in the
|
||||
templates you can specify an email context processor with the
|
||||
templates, you can specify an email context processor with the
|
||||
``mail_context_processor`` decorator. For example::
|
||||
|
||||
security = Security(app, user_datastore)
|
||||
@@ -146,7 +146,7 @@ Emails with Celery
|
||||
------------------
|
||||
|
||||
Sometimes it makes sense to send emails via a task queue, such as `Celery`_.
|
||||
To delay the sending of emails you can use the ``@security.send_mail_task``
|
||||
To delay the sending of emails, you can use the ``@security.send_mail_task``
|
||||
decorator like so::
|
||||
|
||||
# Setup the task
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ instance, you may assign roles such as `Admin`, `Editor`, `SuperUser`, or a
|
||||
combination of said roles to a user. Access control is based on the role name
|
||||
and all roles should be uniquely named. This feature is implemented using the
|
||||
`Flask-Principal`_ extension. If you'd like to implement more granular access
|
||||
control you can refer to the Flask-Princpal `documentation on this topic`_.
|
||||
control, you can refer to the Flask-Principal `documentation on this topic`_.
|
||||
|
||||
|
||||
Password Encryption
|
||||
|
||||
+3
-3
@@ -5,7 +5,7 @@ Flask-Security assumes you'll be using libraries such as SQLAlchemy,
|
||||
MongoEngine or Peewee to define a data model that includes a `User` and
|
||||
`Role` model. The fields on your models must follow a particular convention
|
||||
depending on the functionality your app requires. Aside from this, you're
|
||||
free to add any additional fields to your model(s) if you want. At the bear
|
||||
free to add any additional fields to your model(s) if you want. At the bare
|
||||
minimum your `User` and `Role` model should include the following fields:
|
||||
|
||||
**User**
|
||||
@@ -32,7 +32,7 @@ Confirmable
|
||||
^^^^^^^^^^^
|
||||
|
||||
If you enable account confirmation by setting your application's
|
||||
`SECURITY_CONFIRMABLE` configuration value to `True` your `User` model will
|
||||
`SECURITY_CONFIRMABLE` configuration value to `True`, your `User` model will
|
||||
require the following additional field:
|
||||
|
||||
* ``confirmed_at``
|
||||
@@ -41,7 +41,7 @@ Trackable
|
||||
^^^^^^^^^
|
||||
|
||||
If you enable user tracking by setting your application's `SECURITY_TRACKABLE`
|
||||
configuration value to `True` your `User` model will require the following
|
||||
configuration value to `True`, your `User` model will require the following
|
||||
additional fields:
|
||||
|
||||
* ``last_login_at``
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
__version__ = '1.7.2'
|
||||
__version__ = '1.7.4'
|
||||
|
||||
from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user
|
||||
from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore
|
||||
|
||||
@@ -18,6 +18,7 @@ from itsdangerous import URLSafeTimedSerializer
|
||||
from passlib.context import CryptContext
|
||||
from werkzeug.datastructures import ImmutableList
|
||||
from werkzeug.local import LocalProxy
|
||||
from werkzeug.security import safe_str_cmp
|
||||
|
||||
from .utils import config_value as cv, get_config, md5, url_for_security, string_types
|
||||
from .views import create_blueprint
|
||||
@@ -193,7 +194,7 @@ def _token_loader(token):
|
||||
try:
|
||||
data = _security.remember_token_serializer.loads(token)
|
||||
user = _security.datastore.find_user(id=data[0])
|
||||
if user and md5(user.password) == data[1]:
|
||||
if user and safe_str_cmp(md5(user.password), data[1]):
|
||||
return user
|
||||
except:
|
||||
pass
|
||||
@@ -349,6 +350,9 @@ class _SecurityState(object):
|
||||
rv.update(fn())
|
||||
return rv
|
||||
|
||||
def context_processor(self, fn):
|
||||
self._add_ctx_processor(None, fn)
|
||||
|
||||
def forgot_password_context_processor(self, fn):
|
||||
self._add_ctx_processor('forgot_password', fn)
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ class UserDatastore(object):
|
||||
return kwargs
|
||||
|
||||
def get_user(self, id_or_email):
|
||||
"""Returns a user matching the specified ID or email address"""
|
||||
"""Returns a user matching the specified ID or email address."""
|
||||
raise NotImplementedError
|
||||
|
||||
def find_user(self, *args, **kwargs):
|
||||
@@ -97,7 +97,7 @@ class UserDatastore(object):
|
||||
raise NotImplementedError
|
||||
|
||||
def add_role_to_user(self, user, role):
|
||||
"""Adds a role tp a user
|
||||
"""Adds a role to a user.
|
||||
|
||||
:param user: The user to manipulate
|
||||
:param role: The role to add to the user
|
||||
@@ -110,7 +110,7 @@ class UserDatastore(object):
|
||||
return False
|
||||
|
||||
def remove_role_from_user(self, user, role):
|
||||
"""Removes a role from a user
|
||||
"""Removes a role from a user.
|
||||
|
||||
:param user: The user to manipulate
|
||||
:param role: The role to remove from the user
|
||||
@@ -120,6 +120,7 @@ class UserDatastore(object):
|
||||
if role in user.roles:
|
||||
rv = True
|
||||
user.roles.remove(role)
|
||||
self.put(user)
|
||||
return rv
|
||||
|
||||
def toggle_active(self, user):
|
||||
@@ -155,7 +156,7 @@ class UserDatastore(object):
|
||||
|
||||
def find_or_create_role(self, name, **kwargs):
|
||||
"""Returns a role matching the given name or creates it with any
|
||||
additionally provided parameters
|
||||
additionally provided parameters.
|
||||
"""
|
||||
kwargs["name"] = name
|
||||
return self.find_role(name) or self.create_role(**kwargs)
|
||||
@@ -167,7 +168,7 @@ class UserDatastore(object):
|
||||
return self.put(user)
|
||||
|
||||
def delete_user(self, user):
|
||||
"""Delete the specified user
|
||||
"""Deletes the specified user.
|
||||
|
||||
:param user: The user to delete
|
||||
"""
|
||||
@@ -300,7 +301,7 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
|
||||
return user
|
||||
|
||||
def add_role_to_user(self, user, role):
|
||||
"""Adds a role tp a user
|
||||
"""Adds a role to a user.
|
||||
|
||||
:param user: The user to manipulate
|
||||
:param role: The role to add to the user
|
||||
@@ -315,7 +316,7 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
|
||||
return True
|
||||
|
||||
def remove_role_from_user(self, user, role):
|
||||
"""Removes a role from a user
|
||||
"""Removes a role from a user.
|
||||
|
||||
:param user: The user to manipulate
|
||||
:param role: The role to remove from the user
|
||||
|
||||
+16
-4
@@ -75,13 +75,13 @@ def get_form_field_label(key):
|
||||
|
||||
|
||||
def unique_user_email(form, field):
|
||||
if _datastore.find_user(email=field.data) is not None:
|
||||
if _datastore.get_user(field.data) is not None:
|
||||
msg = get_message('EMAIL_ALREADY_ASSOCIATED', email=field.data)[0]
|
||||
raise ValidationError(msg)
|
||||
|
||||
|
||||
def valid_user_email(form, field):
|
||||
form.user = _datastore.find_user(email=field.data)
|
||||
form.user = _datastore.get_user(field.data)
|
||||
if form.user is None:
|
||||
raise ValidationError(get_message('USER_DOES_NOT_EXIST')[0])
|
||||
|
||||
@@ -175,6 +175,14 @@ class ForgotPasswordForm(Form, UserEmailFormMixin):
|
||||
|
||||
submit = SubmitField(get_form_field_label('recover_password'))
|
||||
|
||||
def validate(self):
|
||||
if not super(ForgotPasswordForm, self).validate():
|
||||
return False
|
||||
if requires_confirmation(self.user):
|
||||
self.email.errors.append(get_message('CONFIRMATION_REQUIRED')[0])
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class PasswordlessLoginForm(Form, UserEmailFormMixin):
|
||||
"""The passwordless login form"""
|
||||
@@ -244,8 +252,12 @@ class ConfirmRegisterForm(Form, RegisterFormMixin,
|
||||
pass
|
||||
|
||||
|
||||
class RegisterForm(ConfirmRegisterForm, PasswordConfirmFormMixin):
|
||||
pass
|
||||
class RegisterForm(ConfirmRegisterForm, PasswordConfirmFormMixin,
|
||||
NextFormMixin):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(RegisterForm, self).__init__(*args, **kwargs)
|
||||
if not self.next.data:
|
||||
self.next.data = request.args.get('next', '')
|
||||
|
||||
|
||||
class ResetPasswordForm(Form, NewPasswordFormMixin, PasswordConfirmFormMixin):
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{% if security.registerable or security.recoverable or security.confirmabled %}
|
||||
{% if security.registerable or security.recoverable or security.confirmable %}
|
||||
<h2>Menu</h2>
|
||||
<ul>
|
||||
<li><a href="{{ url_for_security('login') }}">Login</a></li>
|
||||
<li><a href="{{ url_for_security('login') }}{% if 'next' in request.args %}?next={{ request.args.next|urlencode }}{% endif %}">Login</a></li>
|
||||
{% if security.registerable %}
|
||||
<li><a href="{{ url_for_security('register') }}">Register</a><br/></li>
|
||||
<li><a href="{{ url_for_security('register') }}{% if 'next' in request.args %}?next={{ request.args.next|urlencode }}{% endif %}">Register</a><br/></li>
|
||||
{% endif %}
|
||||
{% if security.recoverable %}
|
||||
<li><a href="{{ url_for_security('forgot_password') }}">Forgot password</a><br/></li>
|
||||
|
||||
+22
-10
@@ -99,13 +99,15 @@ def get_hmac(password):
|
||||
|
||||
:param password: The password to sign
|
||||
"""
|
||||
if _security.password_salt is None:
|
||||
salt = _security.password_salt
|
||||
|
||||
if salt is None:
|
||||
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.encode('utf-8'), password.encode('utf-8'), hashlib.sha512)
|
||||
h = hmac.new(encode_string(salt), encode_string(password), hashlib.sha512)
|
||||
return base64.b64encode(h.digest())
|
||||
|
||||
|
||||
@@ -113,7 +115,7 @@ def verify_password(password, password_hash):
|
||||
"""Returns ``True`` if the password matches the supplied hash.
|
||||
|
||||
:param password: A plaintext password to verify
|
||||
:param password_hash: The expected hash value of the password (usually form your database)
|
||||
:param password_hash: The expected hash value of the password (usually from your database)
|
||||
"""
|
||||
if _security.password_hash != 'plaintext':
|
||||
password = get_hmac(password)
|
||||
@@ -133,7 +135,7 @@ def verify_and_update_password(password, user):
|
||||
password = get_hmac(password)
|
||||
verified, new_password = _pwd_context.verify_and_update(password, user.password)
|
||||
if verified and new_password:
|
||||
user.password = new_password
|
||||
user.password = encrypt_password(password)
|
||||
_datastore.put(user)
|
||||
return verified
|
||||
|
||||
@@ -141,7 +143,7 @@ def verify_and_update_password(password, user):
|
||||
def encrypt_password(password):
|
||||
"""Encrypts the specified plaintext password using the configured encryption options.
|
||||
|
||||
:param password: The plaintext passwrod to encrypt
|
||||
:param password: The plaintext password to encrypt
|
||||
"""
|
||||
if _security.password_hash == 'plaintext':
|
||||
return password
|
||||
@@ -149,8 +151,18 @@ def encrypt_password(password):
|
||||
return _pwd_context.encrypt(signed)
|
||||
|
||||
|
||||
def encode_string(string):
|
||||
"""Encodes a string to bytes, if it isn't already.
|
||||
|
||||
:param string: The string to encode"""
|
||||
|
||||
if isinstance(string, text_type):
|
||||
string = string.encode('utf-8')
|
||||
return string
|
||||
|
||||
|
||||
def md5(data):
|
||||
return hashlib.md5(data.encode('ascii')).hexdigest()
|
||||
return hashlib.md5(encode_string(data)).hexdigest()
|
||||
|
||||
|
||||
def do_flash(message, category=None):
|
||||
@@ -196,11 +208,11 @@ def url_for_security(endpoint, **values):
|
||||
|
||||
|
||||
def validate_redirect_url(url):
|
||||
if url is None:
|
||||
if url is None or url.strip() == '':
|
||||
return False
|
||||
url_next = urlsplit(url)
|
||||
url_base = urlsplit(request.host_url)
|
||||
if url_next.netloc and url_next.netloc != url_base.netloc:
|
||||
if (url_next.netloc or url_next.scheme) and url_next.netloc != url_base.netloc:
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -222,8 +234,8 @@ def get_post_login_redirect(declared=None):
|
||||
return get_post_action_redirect('SECURITY_POST_LOGIN_VIEW', declared)
|
||||
|
||||
|
||||
def get_post_register_redirect():
|
||||
return get_post_action_redirect('SECURITY_POST_REGISTER_VIEW')
|
||||
def get_post_register_redirect(declared=None):
|
||||
return get_post_action_redirect('SECURITY_POST_REGISTER_VIEW', declared)
|
||||
|
||||
|
||||
def find_redirect(key):
|
||||
|
||||
+13
-6
@@ -34,7 +34,7 @@ _security = LocalProxy(lambda: current_app.extensions['security'])
|
||||
_datastore = LocalProxy(lambda: _security.datastore)
|
||||
|
||||
|
||||
def _render_json(form, include_auth_token=False):
|
||||
def _render_json(form, include_user=True, include_auth_token=False):
|
||||
has_errors = len(form.errors) > 0
|
||||
|
||||
if has_errors:
|
||||
@@ -42,7 +42,9 @@ def _render_json(form, include_auth_token=False):
|
||||
response = dict(errors=form.errors)
|
||||
else:
|
||||
code = 200
|
||||
response = dict(user=dict(id=str(form.user.id)))
|
||||
response = dict()
|
||||
if include_user:
|
||||
response['user'] = dict(id=str(form.user.id))
|
||||
if include_auth_token:
|
||||
token = form.user.get_auth_token()
|
||||
response['user']['authentication_token'] = token
|
||||
@@ -78,7 +80,7 @@ def login():
|
||||
return redirect(get_post_login_redirect(form.next.data))
|
||||
|
||||
if request.json:
|
||||
return _render_json(form, True)
|
||||
return _render_json(form, include_auth_token=True)
|
||||
|
||||
return _security.render_template(config_value('LOGIN_USER_TEMPLATE'),
|
||||
login_user_form=form,
|
||||
@@ -120,8 +122,13 @@ def register():
|
||||
login_user(user)
|
||||
|
||||
if not request.json:
|
||||
return redirect(get_post_register_redirect())
|
||||
return _render_json(form, True)
|
||||
if 'next' in form:
|
||||
redirect_url = get_post_register_redirect(form.next.data)
|
||||
else:
|
||||
redirect_url = get_post_register_redirect()
|
||||
|
||||
return redirect(redirect_url)
|
||||
return _render_json(form, include_auth_token=True)
|
||||
|
||||
if request.json:
|
||||
return _render_json(form)
|
||||
@@ -247,7 +254,7 @@ def forgot_password():
|
||||
do_flash(*get_message('PASSWORD_RESET_REQUEST', email=form.user.email))
|
||||
|
||||
if request.json:
|
||||
return _render_json(form)
|
||||
return _render_json(form, include_user=False)
|
||||
|
||||
return _security.render_template(config_value('FORGOT_PASSWORD_TEMPLATE'),
|
||||
forgot_password_form=form,
|
||||
|
||||
@@ -44,7 +44,7 @@ class PyTest(TestCommand):
|
||||
|
||||
setup(
|
||||
name='Flask-Security',
|
||||
version='1.7.2',
|
||||
version='1.7.4',
|
||||
url='https://github.com/mattupstate/flask-security',
|
||||
license='MIT',
|
||||
author='Matt Wright',
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
CUSTOM CHANGE PASSWORD
|
||||
|
||||
{{ global }}
|
||||
{{ foo }}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
CUSTOM FORGOT PASSWORD
|
||||
|
||||
{{ global }}
|
||||
{{ foo }}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
CUSTOM LOGIN USER
|
||||
|
||||
{{ global }}
|
||||
{{ foo }}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
CUSTOM REGISTER USER
|
||||
|
||||
{{ global }}
|
||||
{{ foo }}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
CUSTOM RESET PASSWORD
|
||||
|
||||
{{ global }}
|
||||
{{ foo }}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
CUSTOM SEND CONFIRMATION
|
||||
|
||||
{{ global }}
|
||||
{{ foo }}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
CUSTOM SEND LOGIN
|
||||
|
||||
{{ global }}
|
||||
{{ foo }}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
CUSTOM RESET INSTRUCTIONS
|
||||
|
||||
{{ global }}
|
||||
{{ foo }}
|
||||
|
||||
@@ -40,6 +40,12 @@ def test_authenticate_with_invalid_next(client, get_message):
|
||||
assert get_message('INVALID_REDIRECT') in response.data
|
||||
|
||||
|
||||
def test_authenticate_with_invalid_malformed_next(client, get_message):
|
||||
data = dict(email='matt@lp.com', password='password')
|
||||
response = client.post('/login?next=http:///google.com', data=data)
|
||||
assert get_message('INVALID_REDIRECT') in response.data
|
||||
|
||||
|
||||
def test_authenticate_case_insensitive_email(app, client):
|
||||
response = authenticate(client, 'MATT@lp.com', follow_redirects=True)
|
||||
assert b'Hello matt@lp.com' in response.data
|
||||
|
||||
@@ -23,11 +23,13 @@ def test_view_configuration(client):
|
||||
response = client.get('/custom_login')
|
||||
assert b"<h1>Login</h1>" in response.data
|
||||
|
||||
response = authenticate(client, endpoint='/custom_login', follow_redirects=True)
|
||||
assert b'Post Login' in response.data
|
||||
response = authenticate(client, endpoint='/custom_login')
|
||||
assert 'location' in response.headers
|
||||
assert response.headers['Location'] == 'http://localhost/post_login'
|
||||
|
||||
response = logout(client, endpoint='/custom_logout', follow_redirects=True)
|
||||
assert b'Post Logout' in response.data
|
||||
response = logout(client, endpoint='/custom_logout')
|
||||
assert 'location' in response.headers
|
||||
assert response.headers['Location'] == 'http://localhost/post_logout'
|
||||
|
||||
response = client.get('/http', headers={
|
||||
'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:bogus")
|
||||
|
||||
@@ -35,7 +35,8 @@ def test_confirmable_flag(app, client, sqlalchemy_datastore, get_message):
|
||||
email = 'dude@lp.com'
|
||||
|
||||
with capture_registrations() as registrations:
|
||||
response = client.post('/register', data=dict(email=email, password='password'))
|
||||
data = dict(email=email, password='password', next='')
|
||||
response = client.post('/register', data=data)
|
||||
|
||||
assert response.status_code == 302
|
||||
|
||||
@@ -85,7 +86,8 @@ def test_confirmable_flag(app, client, sqlalchemy_datastore, get_message):
|
||||
|
||||
# Test user was deleted before confirmation
|
||||
with capture_registrations() as registrations:
|
||||
client.post('/register', data=dict(email='mary@lp.com', password='password'))
|
||||
data = dict(email='mary@lp.com', password='password', next='')
|
||||
client.post('/register', data=data)
|
||||
|
||||
user = registrations[0]['user']
|
||||
token = registrations[0]['confirm_token']
|
||||
@@ -102,7 +104,7 @@ def test_confirmable_flag(app, client, sqlalchemy_datastore, get_message):
|
||||
@pytest.mark.settings(confirm_email_within='1 milliseconds')
|
||||
def test_expired_confirmation_token(client, get_message):
|
||||
with capture_registrations() as registrations:
|
||||
data = dict(email='mary@lp.com', password='password')
|
||||
data = dict(email='mary@lp.com', password='password', next='')
|
||||
client.post('/register', data=data, follow_redirects=True)
|
||||
|
||||
user = registrations[0]['user']
|
||||
@@ -118,7 +120,7 @@ def test_expired_confirmation_token(client, get_message):
|
||||
@pytest.mark.registerable()
|
||||
@pytest.mark.settings(login_without_confirmation=True)
|
||||
def test_login_when_unconfirmed(client, get_message):
|
||||
data = dict(email='mary@lp.com', password='password')
|
||||
data = dict(email='mary@lp.com', password='password', next='')
|
||||
response = client.post('/register', data=data, follow_redirects=True)
|
||||
assert b'mary@lp.com' in response.data
|
||||
|
||||
@@ -131,7 +133,8 @@ def test_confirmation_different_user_when_logged_in(client, get_message):
|
||||
|
||||
with capture_registrations() as registrations:
|
||||
for e in e1, e2:
|
||||
client.post('/register', data=dict(email=e, password='password'))
|
||||
data = dict(email=e, password='password', next='')
|
||||
client.post('/register', data=data)
|
||||
logout(client)
|
||||
|
||||
token1 = registrations[0]['confirm_token']
|
||||
@@ -144,3 +147,15 @@ def test_confirmation_different_user_when_logged_in(client, get_message):
|
||||
response = client.get('/confirm/' + token2, follow_redirects=True)
|
||||
assert get_message('EMAIL_CONFIRMED') in response.data
|
||||
assert b'Hello lady@lp.com' in response.data
|
||||
|
||||
|
||||
@pytest.mark.registerable()
|
||||
@pytest.mark.settings(recoverable=True)
|
||||
def test_cannot_reset_password_when_email_is_not_confirmed(client, get_message):
|
||||
email = 'dude@lp.com'
|
||||
|
||||
data = dict(email=email, password='password', next='')
|
||||
response = client.post('/register', data=data, follow_redirects=True)
|
||||
|
||||
response = client.post('/reset', data=dict(email=email), follow_redirects=True)
|
||||
assert get_message('CONFIRMATION_REQUIRED') in response.data
|
||||
|
||||
@@ -24,11 +24,16 @@ from utils import authenticate
|
||||
send_confirmation_template='custom_security/send_confirmation.html',
|
||||
register_user_template='custom_security/register_user.html')
|
||||
def test_context_processors(client, app):
|
||||
@app.security.context_processor
|
||||
def default_ctx_processor():
|
||||
return {'global': 'global'}
|
||||
|
||||
@app.security.forgot_password_context_processor
|
||||
def forgot_password():
|
||||
return {'foo': 'bar'}
|
||||
|
||||
response = client.get('/reset')
|
||||
assert b'global' in response.data
|
||||
assert b'bar' in response.data
|
||||
|
||||
@app.security.login_context_processor
|
||||
@@ -36,6 +41,7 @@ def test_context_processors(client, app):
|
||||
return {'foo': 'bar'}
|
||||
|
||||
response = client.get('/login')
|
||||
assert b'global' in response.data
|
||||
assert b'bar' in response.data
|
||||
|
||||
@app.security.register_context_processor
|
||||
@@ -43,6 +49,7 @@ def test_context_processors(client, app):
|
||||
return {'foo': 'bar'}
|
||||
|
||||
response = client.get('/register')
|
||||
assert b'global' in response.data
|
||||
assert b'bar' in response.data
|
||||
|
||||
@app.security.reset_password_context_processor
|
||||
@@ -50,6 +57,7 @@ def test_context_processors(client, app):
|
||||
return {'foo': 'bar'}
|
||||
|
||||
response = client.get('/reset')
|
||||
assert b'global' in response.data
|
||||
assert b'bar' in response.data
|
||||
|
||||
@app.security.change_password_context_processor
|
||||
@@ -58,6 +66,7 @@ def test_context_processors(client, app):
|
||||
|
||||
authenticate(client)
|
||||
response = client.get('/change')
|
||||
assert b'global' in response.data
|
||||
assert b'bar' in response.data
|
||||
|
||||
@app.security.send_confirmation_context_processor
|
||||
@@ -65,6 +74,7 @@ def test_context_processors(client, app):
|
||||
return {'foo': 'bar'}
|
||||
|
||||
response = client.get('/confirm')
|
||||
assert b'global' in response.data
|
||||
assert b'bar' in response.data
|
||||
|
||||
@app.security.mail_context_processor
|
||||
@@ -75,6 +85,7 @@ def test_context_processors(client, app):
|
||||
client.post('/reset', data=dict(email='matt@lp.com'))
|
||||
|
||||
email = outbox[0]
|
||||
assert 'global' in email.html
|
||||
assert 'bar' in email.html
|
||||
|
||||
|
||||
|
||||
+22
-1
@@ -12,7 +12,7 @@ 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
|
||||
from flask_security.utils import capture_reset_password_requests
|
||||
from flask_security.utils import capture_reset_password_requests, md5, string_types
|
||||
|
||||
from utils import authenticate, init_app_with_options, populate_data
|
||||
|
||||
@@ -170,3 +170,24 @@ def test_change_hash_type(app, sqlalchemy_datastore):
|
||||
|
||||
response = client.post('/login', data=dict(email='matt@lp.com', password='password'))
|
||||
assert response.status_code == 302
|
||||
|
||||
response = client.get('/logout')
|
||||
|
||||
response = client.post('/login', data=dict(email='matt@lp.com', password='password'))
|
||||
assert response.status_code == 302
|
||||
|
||||
|
||||
def test_md5():
|
||||
data = md5(b'hello')
|
||||
assert isinstance(data, string_types)
|
||||
data = md5(u'hellö')
|
||||
assert isinstance(data, string_types)
|
||||
|
||||
|
||||
@pytest.mark.settings(password_salt=u'öööööööööööööööööööööööööööööööööö',
|
||||
password_hash='bcrypt')
|
||||
def test_password_unicode_password_salt(client):
|
||||
response = authenticate(client)
|
||||
assert response.status_code == 302
|
||||
response = authenticate(client, follow_redirects=True)
|
||||
assert b'Hello matt@lp.com' in response.data
|
||||
|
||||
@@ -71,7 +71,7 @@ def test_recoverable_flag(app, client, get_message):
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
assert response.headers['Content-Type'] == 'application/json'
|
||||
assert 'user' in response.jdata['response']
|
||||
assert 'user' not in response.jdata['response']
|
||||
|
||||
logout(client)
|
||||
|
||||
|
||||
@@ -28,7 +28,10 @@ def test_registerable_flag(client, app, get_message):
|
||||
def on_user_registerd(app, user, confirm_token):
|
||||
recorded.append(user)
|
||||
|
||||
data = dict(email='dude@lp.com', password='password', password_confirm='password')
|
||||
data = dict(
|
||||
email='dude@lp.com', password='password', password_confirm='password',
|
||||
next=''
|
||||
)
|
||||
with app.mail.record_messages() as outbox:
|
||||
response = client.post('/register', data=data, follow_redirects=True)
|
||||
|
||||
@@ -45,10 +48,21 @@ def test_registerable_flag(client, app, get_message):
|
||||
logout(client)
|
||||
|
||||
# Test registering with an existing email
|
||||
data = dict(email='dude@lp.com', password='password', password_confirm='password')
|
||||
data = dict(
|
||||
email='dude@lp.com', password='password', password_confirm='password',
|
||||
next=''
|
||||
)
|
||||
response = client.post('/register', data=data, follow_redirects=True)
|
||||
assert get_message('EMAIL_ALREADY_ASSOCIATED', email='dude@lp.com') in response.data
|
||||
|
||||
# Test registering with an existing email but case insensitive
|
||||
data = dict(
|
||||
email='Dude@lp.com', password='password', password_confirm='password',
|
||||
next=''
|
||||
)
|
||||
response = client.post('/register', data=data, follow_redirects=True)
|
||||
assert get_message('EMAIL_ALREADY_ASSOCIATED', email='Dude@lp.com') in response.data
|
||||
|
||||
# Test registering with JSON
|
||||
data = '{ "email": "dude2@lp.com", "password": "password"}'
|
||||
response = client.post('/register', data=data, headers={'Content-Type': 'application/json'})
|
||||
@@ -68,7 +82,8 @@ def test_registerable_flag(client, app, get_message):
|
||||
# Test ?next param
|
||||
data = dict(email='dude3@lp.com',
|
||||
password='password',
|
||||
password_confirm='password')
|
||||
password_confirm='password',
|
||||
next='')
|
||||
|
||||
response = client.post('/register?next=/page1', data=data, follow_redirects=True)
|
||||
assert b'Page 1' in response.data
|
||||
@@ -81,7 +96,8 @@ def test_custom_register_url(client):
|
||||
|
||||
data = dict(email='dude@lp.com',
|
||||
password='password',
|
||||
password_confirm='password')
|
||||
password_confirm='password',
|
||||
next='')
|
||||
|
||||
response = client.post('/custom_register', data=data, follow_redirects=True)
|
||||
assert b'Post Register' in response.data
|
||||
@@ -95,7 +111,10 @@ def test_custom_register_tempalate(client):
|
||||
|
||||
@pytest.mark.settings(send_register_email=False)
|
||||
def test_disable_register_emails(client, app):
|
||||
data = dict(email='dude@lp.com', password='password', password_confirm='password')
|
||||
data = dict(
|
||||
email='dude@lp.com', password='password', password_confirm='password',
|
||||
next=''
|
||||
)
|
||||
with app.mail.record_messages() as outbox:
|
||||
client.post('/register', data=data, follow_redirects=True)
|
||||
assert len(outbox) == 0
|
||||
|
||||
Reference in New Issue
Block a user