Compare commits

...
40 Commits
Author SHA1 Message Date
Matt Wright c7d0ea9cce Add additional item to CHANGES 2014-10-13 13:47:35 -04:00
Matt Wright 94c7c09dc2 Bump version number to 1.7.4 2014-10-13 13:38:44 -04:00
Matt Wright f6405797f1 Update CHANGES 2014-10-13 13:38:16 -04:00
Matt Wright 2cffb6634d Fix case sensitivity when searching for users by email address. Fixes #323. 2014-10-13 13:34:11 -04:00
Matt Wright 824a52b883 Merge branch 'develop' of github.com:mattupstate/flask-security into develop 2014-10-13 13:25:08 -04:00
Matt Wright fe7e4c4afc Add additional steps to the test_change_hash_type and make change to verify_and_update_password to make the test pass. Fixes #328. 2014-10-13 13:25:01 -04:00
Matt Wright 591bc27a5e Merge pull request #318 from boydgreenfield/develop
Prevent open redirects when a malformed URL is passed to ?next=
2014-10-13 11:54:32 -04:00
Nick Greenfield 5bc37add88 Update to use (url_next.netloc or url_next.scheme) in the validate_redirect_url open redirect patch. 2014-10-01 09:49:00 -07:00
Nick Greenfield 8b036f2a3e Prevent open redirects when a malformed URL is passed to ?next=
Example: "/login?next=http:///google.com" (note 3rd slash)
2014-09-26 11:08:58 -07:00
Matt Wright 76ad77a233 Compare a string (not bytes) for PY3 support 2014-09-17 11:39:19 -04:00
Matt Wright 679cee7969 Add default/global context processor. Fixes #306 2014-09-17 11:27:44 -04:00
Matt Wright 3d7b97ac31 Forgot password form should not validate if user has not confirmed their email address yet. Fixes #298 2014-09-17 11:21:31 -04:00
Matt Wright 3a0af73231 Merge pull request #281 from sjml/patch-1
Fixing typo in documentation
2014-09-17 10:03:53 -04:00
Matt Wright 3458391791 Merge pull request #284 from tony/patch-1
Typo
2014-09-17 10:02:27 -04:00
Matt Wright 37908ca335 Merge pull request #303 from graup/patch-1
Docs: Fixed typo and made punctuation more consistent.
2014-09-17 10:02:16 -04:00
Matt Wright f83cd56ff2 Merge pull request #311 from moorereason/develop
Fix small issues in docs
2014-09-17 10:01:57 -04:00
Cameron Moore c10ec29fe5 Fix small issues in docs 2014-09-07 21:37:33 -05:00
Matt Wright d2fe7aefcb Merge pull request #307 from mickey06/develop
Save changes to db after removal of role from user
2014-08-29 12:19:26 -04:00
Matt Wright 708ddeb6c4 Merge pull request #309 from gregeinfrank/develop
Fix two typos in docstrings
2014-08-29 12:15:20 -04:00
Greg Einfrank 2aeee348d4 Fix two typos in docstrings 2014-08-27 23:37:58 -04:00
Khalil El Kouhen 52b177cd2e Save changes to db after removal of role from user 2014-08-26 16:43:23 +01:00
Paul Grau dab2fc8c8b Docs: Fixed typo and made punctuation more consistent. 2014-08-19 18:01:03 +09:00
Matt Wright 31e3ab5470 Merge pull request #289 from scollinson/fix_menu_typo
Fix a typo in the menu template
2014-08-13 15:44:20 -04:00
Matt Wright 6bdcd2930d Merge pull request #278 from dokterbob/post_register_redirect
Enable ‘next’ redirection after registration.
2014-07-29 13:23:42 -04:00
Sam Collinson 1076887900 fix typo in menu template 2014-07-21 19:07:12 +12:00
Tony Narlock 285fe888da Typo
s/tempalte/template
2014-07-16 14:35:47 -07:00
Shane Liesegang bf139847cb Fixing typo in documentation
Just a small usage error. :)
2014-07-11 23:45:00 -07:00
Mathijs de Bruin 15c9ef1d07 Attempt to fix tests. 2014-07-03 15:48:55 +02:00
Mathijs de Bruin d1ec38ea18 Keep value of ‘next’ in menu links.
This allows moving from the login to registration (and vice versa) while keeping the redirection URL, enabling a much smoother user experience.
2014-07-03 15:22:56 +02:00
Mathijs de Bruin 577a3a8110 Enable ‘next’ redirection after registration. 2014-07-03 15:21:03 +02:00
Matt Wright 7b474efd7a Bump version number to 1.7.3 2014-06-10 13:05:24 -04:00
Matt Wright f12bc17e7e Merge branch 'develop' 2014-06-10 13:05:12 -04:00
Matt Wright 999f882f61 Update CHANGES 2014-06-10 13:05:06 -04:00
Matt Wright a140c01b57 Add python3.4 to travis and tox 2014-06-10 12:40:12 -04:00
Matt Wright 76cf3eaf6a Do not expose user info in /reset responses. Fixes #249 2014-06-10 12:24:19 -04:00
Matt Wright a6b5d3053c Use safe_str_cmp when evaluating tokens. Fixes #252 2014-06-10 12:14:58 -04:00
Matt Wright 0a48997fdd Improve encoding of strings. Addresses #231 and #253 2014-06-10 11:47:35 -04:00
Matt Wright 96f1b3e0d1 Fix tests to pass python 3 2014-06-10 11:12:40 -04:00
Matt Wright 0facdaacd9 Make validate_redirect_url smarter. Fixes #261. 2014-06-10 10:48:56 -04:00
Matt Wright 9cadf855a4 Adjust POST_LOGIN_VIEW and POST_LOGOUT_VIEW test 2014-06-10 10:42:28 -04:00
31 changed files with 211 additions and 78 deletions
+1
View File
@@ -4,6 +4,7 @@ python:
- "2.6" - "2.6"
- "2.7" - "2.7"
- "3.3" - "3.3"
- "3.4"
- "pypy" - "pypy"
install: install:
+22
View File
@@ -3,6 +3,28 @@ Flask-Security Changelog
Here you can see the full list of changes between each Flask-Security release. 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 Version 1.7.2
------------- -------------
+1 -1
View File
@@ -49,7 +49,7 @@ copyright = u'2012, Matt Wright'
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = '1.7.2' version = '1.7.4'
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = version release = version
+10 -10
View File
@@ -15,7 +15,7 @@ Core
``SECURITY_URL_PREFIX`` Specifies the URL prefix for the ``SECURITY_URL_PREFIX`` Specifies the URL prefix for the
Flask-Security blueprint. Defaults to Flask-Security blueprint. Defaults to
``None``. ``None``.
``SECURITY_FLASH_MESSAGES`` Specifies wether or not to flash ``SECURITY_FLASH_MESSAGES`` Specifies whether or not to flash
messages during security procedures. messages during security procedures.
Defaults to ``True``. Defaults to ``True``.
``SECURITY_PASSWORD_HASH`` Specifies the password hash algorithm to ``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 ``SECURITY_CONFIRM_ERROR_VIEW`` Specifies the view to redirect to if a
confirmation error occurs. This value can be set confirmation error occurs. This value can be set
to a URL or an endpoint name. If this value is 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 to resend a confirmation link. Defaults to
``None``. ``None``.
``SECURITY_POST_REGISTER_VIEW`` Specifies the view to redirect to after a user ``SECURITY_POST_REGISTER_VIEW`` Specifies the view to redirect to after a user
successfully registers. This value can be set to successfully registers. This value can be set to
a URL or an endpoint name. If this value is 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 ``SECURITY_POST_LOGIN_VIEW``. Defaults to
``None``. ``None``.
``SECURITY_POST_CONFIRM_VIEW`` Specifies the view to redirect to after a user ``SECURITY_POST_CONFIRM_VIEW`` Specifies the view to redirect to after a user
successfully confirms their email. This value successfully confirms their email. This value
can be set to a URL or an endpoint name. If this 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 value of ``SECURITY_POST_LOGIN_VIEW``. Defaults
to ``None``. to ``None``.
``SECURITY_POST_RESET_VIEW`` Specifies the view to redirect to after a user ``SECURITY_POST_RESET_VIEW`` Specifies the view to redirect to after a user
successfully resets their password. This value successfully resets their password. This value
can be set to a URL or an endpoint name. If this 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 value of ``SECURITY_POST_LOGIN_VIEW``. Defaults
to ``None``. to ``None``.
``SECURITY_POST_CHANGE_VIEW`` Specifies the view to redirect to after a user ``SECURITY_POST_CHANGE_VIEW`` Specifies the view to redirect to after a user
successfully changes their password. This value successfully changes their password. This value
can be set to a URL or an endpoint name. If this 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 value of ``SECURITY_POST_LOGIN_VIEW``. Defaults
to ``None``. to ``None``.
``SECURITY_UNAUTHORIZED_VIEW`` Specifies the view to redirect to if a user ``SECURITY_UNAUTHORIZED_VIEW`` Specifies the view to redirect to if a user
attempts to access a URL/endpoint that they do attempts to access a URL/endpoint that they do
not have permission to access. If this value is 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``. HTTP 403 response. Defaults to ``None``.
=============================== ================================================ =============================== ================================================
@@ -144,7 +144,7 @@ Feature Flags
========================= ====================================================== ========================= ======================================================
``SECURITY_CONFIRMABLE`` Specifies if users are required to confirm their email ``SECURITY_CONFIRMABLE`` Specifies if users are required to confirm their email
address when registering a new account. If this value 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 confirmations and requests to resend confirmation
instructions. The URL for this endpoint is specified instructions. The URL for this endpoint is specified
by the ``SECURITY_CONFIRM_URL`` configuration option. by the ``SECURITY_CONFIRM_URL`` configuration option.
@@ -158,11 +158,11 @@ Feature Flags
specified by the ``SECURITY_RESET_URL`` configuration specified by the ``SECURITY_RESET_URL`` configuration
option. Defaults to ``False``. option. Defaults to ``False``.
``SECURITY_TRACKABLE`` Specifies if Flask-Security should track basic user ``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 models have the required fields/attribues. Defaults to
``False`` ``False``
``SECURITY_PASSWORDLESS`` Specifies if Flask-Security should enable the ``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 are not required to enter a password to login but are
sent an email with a login link. This feature is sent an email with a login link. This feature is
experimental and should be used with caution. Defaults experimental and should be used with caution. Defaults
+5 -5
View File
@@ -2,7 +2,7 @@ Customizing Views
================= =================
Flask-Security bootstraps your application with various views for handling its 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 you'll probably want to change the way these views look to be more in line with
your application's visual design. your application's visual design.
@@ -36,7 +36,7 @@ Flask application context processor:
* ``<template_name>_form``: A form object for the view * ``<template_name>_form``: A form object for the view
* ``security``: The Flask-Security extension object * ``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:: for all views or a specific view. For example::
security = Security(app, user_datastore) security = Security(app, user_datastore)
@@ -106,7 +106,7 @@ The following is a list of all the available form overrides:
Emails 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``. may send. Templates are located within the subfolder named ``security/email``.
The following is a list of email templates: 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 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 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:: ``mail_context_processor`` decorator. For example::
security = Security(app, user_datastore) 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`_. 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:: decorator like so::
# Setup the task # Setup the task
+1 -1
View File
@@ -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 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 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 `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 Password Encryption
+3 -3
View File
@@ -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 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 `Role` model. The fields on your models must follow a particular convention
depending on the functionality your app requires. Aside from this, you're 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: minimum your `User` and `Role` model should include the following fields:
**User** **User**
@@ -32,7 +32,7 @@ Confirmable
^^^^^^^^^^^ ^^^^^^^^^^^
If you enable account confirmation by setting your application's 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: require the following additional field:
* ``confirmed_at`` * ``confirmed_at``
@@ -41,7 +41,7 @@ Trackable
^^^^^^^^^ ^^^^^^^^^
If you enable user tracking by setting your application's `SECURITY_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: additional fields:
* ``last_login_at`` * ``last_login_at``
+1 -1
View File
@@ -10,7 +10,7 @@
:license: MIT, see LICENSE for more details. :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 .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user
from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore
+5 -1
View File
@@ -18,6 +18,7 @@ from itsdangerous import URLSafeTimedSerializer
from passlib.context import CryptContext from passlib.context import CryptContext
from werkzeug.datastructures import ImmutableList from werkzeug.datastructures import ImmutableList
from werkzeug.local import LocalProxy 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 .utils import config_value as cv, get_config, md5, url_for_security, string_types
from .views import create_blueprint from .views import create_blueprint
@@ -193,7 +194,7 @@ def _token_loader(token):
try: try:
data = _security.remember_token_serializer.loads(token) data = _security.remember_token_serializer.loads(token)
user = _security.datastore.find_user(id=data[0]) 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 return user
except: except:
pass pass
@@ -349,6 +350,9 @@ class _SecurityState(object):
rv.update(fn()) rv.update(fn())
return rv return rv
def context_processor(self, fn):
self._add_ctx_processor(None, fn)
def forgot_password_context_processor(self, fn): def forgot_password_context_processor(self, fn):
self._add_ctx_processor('forgot_password', fn) self._add_ctx_processor('forgot_password', fn)
+8 -7
View File
@@ -85,7 +85,7 @@ class UserDatastore(object):
return kwargs return kwargs
def get_user(self, id_or_email): 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 raise NotImplementedError
def find_user(self, *args, **kwargs): def find_user(self, *args, **kwargs):
@@ -97,7 +97,7 @@ class UserDatastore(object):
raise NotImplementedError raise NotImplementedError
def add_role_to_user(self, user, role): 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 user: The user to manipulate
:param role: The role to add to the user :param role: The role to add to the user
@@ -110,7 +110,7 @@ class UserDatastore(object):
return False return False
def remove_role_from_user(self, user, role): 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 user: The user to manipulate
:param role: The role to remove from the user :param role: The role to remove from the user
@@ -120,6 +120,7 @@ class UserDatastore(object):
if role in user.roles: if role in user.roles:
rv = True rv = True
user.roles.remove(role) user.roles.remove(role)
self.put(user)
return rv return rv
def toggle_active(self, user): def toggle_active(self, user):
@@ -155,7 +156,7 @@ class UserDatastore(object):
def find_or_create_role(self, name, **kwargs): def find_or_create_role(self, name, **kwargs):
"""Returns a role matching the given name or creates it with any """Returns a role matching the given name or creates it with any
additionally provided parameters additionally provided parameters.
""" """
kwargs["name"] = name kwargs["name"] = name
return self.find_role(name) or self.create_role(**kwargs) return self.find_role(name) or self.create_role(**kwargs)
@@ -167,7 +168,7 @@ class UserDatastore(object):
return self.put(user) return self.put(user)
def delete_user(self, user): def delete_user(self, user):
"""Delete the specified user """Deletes the specified user.
:param user: The user to delete :param user: The user to delete
""" """
@@ -300,7 +301,7 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
return user return user
def add_role_to_user(self, user, role): 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 user: The user to manipulate
:param role: The role to add to the user :param role: The role to add to the user
@@ -315,7 +316,7 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
return True return True
def remove_role_from_user(self, user, role): 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 user: The user to manipulate
:param role: The role to remove from the user :param role: The role to remove from the user
+16 -4
View File
@@ -75,13 +75,13 @@ def get_form_field_label(key):
def unique_user_email(form, field): 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] msg = get_message('EMAIL_ALREADY_ASSOCIATED', email=field.data)[0]
raise ValidationError(msg) raise ValidationError(msg)
def valid_user_email(form, field): 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: if form.user is None:
raise ValidationError(get_message('USER_DOES_NOT_EXIST')[0]) 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')) 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): class PasswordlessLoginForm(Form, UserEmailFormMixin):
"""The passwordless login form""" """The passwordless login form"""
@@ -244,8 +252,12 @@ class ConfirmRegisterForm(Form, RegisterFormMixin,
pass pass
class RegisterForm(ConfirmRegisterForm, PasswordConfirmFormMixin): class RegisterForm(ConfirmRegisterForm, PasswordConfirmFormMixin,
pass 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): class ResetPasswordForm(Form, NewPasswordFormMixin, PasswordConfirmFormMixin):
+3 -3
View File
@@ -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> <h2>Menu</h2>
<ul> <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 %} {% 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 %} {% endif %}
{% if security.recoverable %} {% if security.recoverable %}
<li><a href="{{ url_for_security('forgot_password') }}">Forgot password</a><br/></li> <li><a href="{{ url_for_security('forgot_password') }}">Forgot password</a><br/></li>
+22 -10
View File
@@ -99,13 +99,15 @@ def get_hmac(password):
:param password: The password to sign :param password: The password to sign
""" """
if _security.password_salt is None: salt = _security.password_salt
if salt is None:
raise RuntimeError( raise RuntimeError(
'The configuration value `SECURITY_PASSWORD_SALT` must ' 'The configuration value `SECURITY_PASSWORD_SALT` must '
'not be None when the value of `SECURITY_PASSWORD_HASH` is ' 'not be None when the value of `SECURITY_PASSWORD_HASH` is '
'set to "%s"' % _security.password_hash) '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()) return base64.b64encode(h.digest())
@@ -113,7 +115,7 @@ def verify_password(password, password_hash):
"""Returns ``True`` if the password matches the supplied hash. """Returns ``True`` if the password matches the supplied hash.
:param password: A plaintext password to verify :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': if _security.password_hash != 'plaintext':
password = get_hmac(password) password = get_hmac(password)
@@ -133,7 +135,7 @@ def verify_and_update_password(password, user):
password = get_hmac(password) password = get_hmac(password)
verified, new_password = _pwd_context.verify_and_update(password, user.password) verified, new_password = _pwd_context.verify_and_update(password, user.password)
if verified and new_password: if verified and new_password:
user.password = new_password user.password = encrypt_password(password)
_datastore.put(user) _datastore.put(user)
return verified return verified
@@ -141,7 +143,7 @@ def verify_and_update_password(password, user):
def encrypt_password(password): def encrypt_password(password):
"""Encrypts the specified plaintext password using the configured encryption options. """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': if _security.password_hash == 'plaintext':
return password return password
@@ -149,8 +151,18 @@ def encrypt_password(password):
return _pwd_context.encrypt(signed) 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): def md5(data):
return hashlib.md5(data.encode('ascii')).hexdigest() return hashlib.md5(encode_string(data)).hexdigest()
def do_flash(message, category=None): def do_flash(message, category=None):
@@ -196,11 +208,11 @@ def url_for_security(endpoint, **values):
def validate_redirect_url(url): def validate_redirect_url(url):
if url is None: if url is None or url.strip() == '':
return False return False
url_next = urlsplit(url) url_next = urlsplit(url)
url_base = urlsplit(request.host_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 False
return True return True
@@ -222,8 +234,8 @@ def get_post_login_redirect(declared=None):
return get_post_action_redirect('SECURITY_POST_LOGIN_VIEW', declared) return get_post_action_redirect('SECURITY_POST_LOGIN_VIEW', declared)
def get_post_register_redirect(): def get_post_register_redirect(declared=None):
return get_post_action_redirect('SECURITY_POST_REGISTER_VIEW') return get_post_action_redirect('SECURITY_POST_REGISTER_VIEW', declared)
def find_redirect(key): def find_redirect(key):
+13 -6
View File
@@ -34,7 +34,7 @@ _security = LocalProxy(lambda: current_app.extensions['security'])
_datastore = LocalProxy(lambda: _security.datastore) _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 has_errors = len(form.errors) > 0
if has_errors: if has_errors:
@@ -42,7 +42,9 @@ def _render_json(form, include_auth_token=False):
response = dict(errors=form.errors) response = dict(errors=form.errors)
else: else:
code = 200 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: if include_auth_token:
token = form.user.get_auth_token() token = form.user.get_auth_token()
response['user']['authentication_token'] = token response['user']['authentication_token'] = token
@@ -78,7 +80,7 @@ def login():
return redirect(get_post_login_redirect(form.next.data)) return redirect(get_post_login_redirect(form.next.data))
if request.json: 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'), return _security.render_template(config_value('LOGIN_USER_TEMPLATE'),
login_user_form=form, login_user_form=form,
@@ -120,8 +122,13 @@ def register():
login_user(user) login_user(user)
if not request.json: if not request.json:
return redirect(get_post_register_redirect()) if 'next' in form:
return _render_json(form, True) 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: if request.json:
return _render_json(form) return _render_json(form)
@@ -247,7 +254,7 @@ def forgot_password():
do_flash(*get_message('PASSWORD_RESET_REQUEST', email=form.user.email)) do_flash(*get_message('PASSWORD_RESET_REQUEST', email=form.user.email))
if request.json: if request.json:
return _render_json(form) return _render_json(form, include_user=False)
return _security.render_template(config_value('FORGOT_PASSWORD_TEMPLATE'), return _security.render_template(config_value('FORGOT_PASSWORD_TEMPLATE'),
forgot_password_form=form, forgot_password_form=form,
+1 -1
View File
@@ -44,7 +44,7 @@ class PyTest(TestCommand):
setup( setup(
name='Flask-Security', name='Flask-Security',
version='1.7.2', version='1.7.4',
url='https://github.com/mattupstate/flask-security', url='https://github.com/mattupstate/flask-security',
license='MIT', license='MIT',
author='Matt Wright', author='Matt Wright',
@@ -1,3 +1,3 @@
CUSTOM CHANGE PASSWORD CUSTOM CHANGE PASSWORD
{{ global }}
{{ foo }} {{ foo }}
@@ -1,3 +1,3 @@
CUSTOM FORGOT PASSWORD CUSTOM FORGOT PASSWORD
{{ global }}
{{ foo }} {{ foo }}
@@ -1,3 +1,3 @@
CUSTOM LOGIN USER CUSTOM LOGIN USER
{{ global }}
{{ foo }} {{ foo }}
@@ -1,3 +1,3 @@
CUSTOM REGISTER USER CUSTOM REGISTER USER
{{ global }}
{{ foo }} {{ foo }}
@@ -1,3 +1,3 @@
CUSTOM RESET PASSWORD CUSTOM RESET PASSWORD
{{ global }}
{{ foo }} {{ foo }}
@@ -1,3 +1,3 @@
CUSTOM SEND CONFIRMATION CUSTOM SEND CONFIRMATION
{{ global }}
{{ foo }} {{ foo }}
@@ -1,3 +1,3 @@
CUSTOM SEND LOGIN CUSTOM SEND LOGIN
{{ global }}
{{ foo }} {{ foo }}
@@ -1,3 +1,3 @@
CUSTOM RESET INSTRUCTIONS CUSTOM RESET INSTRUCTIONS
{{ global }}
{{ foo }} {{ foo }}
+6
View File
@@ -40,6 +40,12 @@ def test_authenticate_with_invalid_next(client, get_message):
assert get_message('INVALID_REDIRECT') in response.data 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): def test_authenticate_case_insensitive_email(app, client):
response = authenticate(client, 'MATT@lp.com', follow_redirects=True) response = authenticate(client, 'MATT@lp.com', follow_redirects=True)
assert b'Hello matt@lp.com' in response.data assert b'Hello matt@lp.com' in response.data
+6 -4
View File
@@ -23,11 +23,13 @@ def test_view_configuration(client):
response = client.get('/custom_login') response = client.get('/custom_login')
assert b"<h1>Login</h1>" in response.data assert b"<h1>Login</h1>" in response.data
response = authenticate(client, endpoint='/custom_login', follow_redirects=True) response = authenticate(client, endpoint='/custom_login')
assert b'Post Login' in response.data assert 'location' in response.headers
assert response.headers['Location'] == 'http://localhost/post_login'
response = logout(client, endpoint='/custom_logout', follow_redirects=True) response = logout(client, endpoint='/custom_logout')
assert b'Post Logout' in response.data assert 'location' in response.headers
assert response.headers['Location'] == 'http://localhost/post_logout'
response = client.get('/http', headers={ response = client.get('/http', headers={
'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:bogus") 'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:bogus")
+20 -5
View File
@@ -35,7 +35,8 @@ def test_confirmable_flag(app, client, sqlalchemy_datastore, get_message):
email = 'dude@lp.com' email = 'dude@lp.com'
with capture_registrations() as registrations: 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 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 # Test user was deleted before confirmation
with capture_registrations() as registrations: 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'] user = registrations[0]['user']
token = registrations[0]['confirm_token'] 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') @pytest.mark.settings(confirm_email_within='1 milliseconds')
def test_expired_confirmation_token(client, get_message): def test_expired_confirmation_token(client, get_message):
with capture_registrations() as registrations: 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) client.post('/register', data=data, follow_redirects=True)
user = registrations[0]['user'] user = registrations[0]['user']
@@ -118,7 +120,7 @@ def test_expired_confirmation_token(client, get_message):
@pytest.mark.registerable() @pytest.mark.registerable()
@pytest.mark.settings(login_without_confirmation=True) @pytest.mark.settings(login_without_confirmation=True)
def test_login_when_unconfirmed(client, get_message): 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) response = client.post('/register', data=data, follow_redirects=True)
assert b'mary@lp.com' in response.data 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: with capture_registrations() as registrations:
for e in e1, e2: 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) logout(client)
token1 = registrations[0]['confirm_token'] 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) response = client.get('/confirm/' + token2, follow_redirects=True)
assert get_message('EMAIL_CONFIRMED') in response.data assert get_message('EMAIL_CONFIRMED') in response.data
assert b'Hello lady@lp.com' 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
+11
View File
@@ -24,11 +24,16 @@ from utils import authenticate
send_confirmation_template='custom_security/send_confirmation.html', send_confirmation_template='custom_security/send_confirmation.html',
register_user_template='custom_security/register_user.html') register_user_template='custom_security/register_user.html')
def test_context_processors(client, app): def test_context_processors(client, app):
@app.security.context_processor
def default_ctx_processor():
return {'global': 'global'}
@app.security.forgot_password_context_processor @app.security.forgot_password_context_processor
def forgot_password(): def forgot_password():
return {'foo': 'bar'} return {'foo': 'bar'}
response = client.get('/reset') response = client.get('/reset')
assert b'global' in response.data
assert b'bar' in response.data assert b'bar' in response.data
@app.security.login_context_processor @app.security.login_context_processor
@@ -36,6 +41,7 @@ def test_context_processors(client, app):
return {'foo': 'bar'} return {'foo': 'bar'}
response = client.get('/login') response = client.get('/login')
assert b'global' in response.data
assert b'bar' in response.data assert b'bar' in response.data
@app.security.register_context_processor @app.security.register_context_processor
@@ -43,6 +49,7 @@ def test_context_processors(client, app):
return {'foo': 'bar'} return {'foo': 'bar'}
response = client.get('/register') response = client.get('/register')
assert b'global' in response.data
assert b'bar' in response.data assert b'bar' in response.data
@app.security.reset_password_context_processor @app.security.reset_password_context_processor
@@ -50,6 +57,7 @@ def test_context_processors(client, app):
return {'foo': 'bar'} return {'foo': 'bar'}
response = client.get('/reset') response = client.get('/reset')
assert b'global' in response.data
assert b'bar' in response.data assert b'bar' in response.data
@app.security.change_password_context_processor @app.security.change_password_context_processor
@@ -58,6 +66,7 @@ def test_context_processors(client, app):
authenticate(client) authenticate(client)
response = client.get('/change') response = client.get('/change')
assert b'global' in response.data
assert b'bar' in response.data assert b'bar' in response.data
@app.security.send_confirmation_context_processor @app.security.send_confirmation_context_processor
@@ -65,6 +74,7 @@ def test_context_processors(client, app):
return {'foo': 'bar'} return {'foo': 'bar'}
response = client.get('/confirm') response = client.get('/confirm')
assert b'global' in response.data
assert b'bar' in response.data assert b'bar' in response.data
@app.security.mail_context_processor @app.security.mail_context_processor
@@ -75,6 +85,7 @@ def test_context_processors(client, app):
client.post('/reset', data=dict(email='matt@lp.com')) client.post('/reset', data=dict(email='matt@lp.com'))
email = outbox[0] email = outbox[0]
assert 'global' in email.html
assert 'bar' in email.html assert 'bar' in email.html
+22 -1
View File
@@ -12,7 +12,7 @@ from flask_security import Security
from flask_security.forms import LoginForm, RegisterForm, ConfirmRegisterForm, \ from flask_security.forms import LoginForm, RegisterForm, ConfirmRegisterForm, \
SendConfirmationForm, PasswordlessLoginForm, ForgotPasswordForm, ResetPasswordForm, \ SendConfirmationForm, PasswordlessLoginForm, ForgotPasswordForm, ResetPasswordForm, \
ChangePasswordForm, TextField, PasswordField, email_required, email_validator, valid_user_email 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 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')) response = client.post('/login', data=dict(email='matt@lp.com', password='password'))
assert response.status_code == 302 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
+1 -1
View File
@@ -71,7 +71,7 @@ def test_recoverable_flag(app, client, get_message):
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}) })
assert response.headers['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) logout(client)
+24 -5
View File
@@ -28,7 +28,10 @@ def test_registerable_flag(client, app, get_message):
def on_user_registerd(app, user, confirm_token): def on_user_registerd(app, user, confirm_token):
recorded.append(user) 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: with app.mail.record_messages() as outbox:
response = client.post('/register', data=data, follow_redirects=True) response = client.post('/register', data=data, follow_redirects=True)
@@ -45,10 +48,21 @@ def test_registerable_flag(client, app, get_message):
logout(client) logout(client)
# Test registering with an existing email # 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) response = client.post('/register', data=data, follow_redirects=True)
assert get_message('EMAIL_ALREADY_ASSOCIATED', email='dude@lp.com') in response.data 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 # Test registering with JSON
data = '{ "email": "dude2@lp.com", "password": "password"}' data = '{ "email": "dude2@lp.com", "password": "password"}'
response = client.post('/register', data=data, headers={'Content-Type': 'application/json'}) 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 # Test ?next param
data = dict(email='dude3@lp.com', data = dict(email='dude3@lp.com',
password='password', password='password',
password_confirm='password') password_confirm='password',
next='')
response = client.post('/register?next=/page1', data=data, follow_redirects=True) response = client.post('/register?next=/page1', data=data, follow_redirects=True)
assert b'Page 1' in response.data assert b'Page 1' in response.data
@@ -81,7 +96,8 @@ def test_custom_register_url(client):
data = dict(email='dude@lp.com', data = dict(email='dude@lp.com',
password='password', password='password',
password_confirm='password') password_confirm='password',
next='')
response = client.post('/custom_register', data=data, follow_redirects=True) response = client.post('/custom_register', data=data, follow_redirects=True)
assert b'Post Register' in response.data assert b'Post Register' in response.data
@@ -95,7 +111,10 @@ def test_custom_register_tempalate(client):
@pytest.mark.settings(send_register_email=False) @pytest.mark.settings(send_register_email=False)
def test_disable_register_emails(client, app): 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: with app.mail.record_messages() as outbox:
client.post('/register', data=data, follow_redirects=True) client.post('/register', data=data, follow_redirects=True)
assert len(outbox) == 0 assert len(outbox) == 0
+1 -1
View File
@@ -1,5 +1,5 @@
[tox] [tox]
envlist = py26, py27, py33, pypy envlist = py26, py27, py33, py34, pypy
[testenv] [testenv]
deps = deps =