Compare commits

...
44 Commits
Author SHA1 Message Date
Matt Wright d907e5b890 Merge pull request #363 from jeffwidman/patch-1
"overrided" --> "overriden"
2015-05-02 13:50:39 -04:00
Matt Wright 79fe821c80 Merge pull request #351 from denizdogan/master
Add documentation about SECURITY_MSG configuration variables.
2015-05-02 13:45:13 -04:00
Jeff Widman fccaccd282 "overrided" --> "overriden"
Per http://english.stackexchange.com/questions/75786/overrode-vs-overridden
2015-02-02 23:34:52 -08:00
Deniz Dogan f4d758f561 Add documentation about SECURITY_MSG configuration variables. 2014-12-21 15:20:48 +01:00
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 253 additions and 79 deletions
+1
View File
@@ -4,6 +4,7 @@ python:
- "2.6"
- "2.7"
- "3.3"
- "3.4"
- "pypy"
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.
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
View File
@@ -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
+51 -10
View File
@@ -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
@@ -261,3 +261,44 @@ Miscellaneous
me" value used when logging in
a user. Defaults to ``False``.
============================================= ==================================
Messages
-------------
The following are the messages Flask-Security uses. They are tuples; the first
element is the message and the second element is the error level.
The default messages and error levels can be found in ``core.py``.
* ``SECURITY_MSG_ALREADY_CONFIRMED``
* ``SECURITY_MSG_CONFIRMATION_EXPIRED``
* ``SECURITY_MSG_CONFIRMATION_REQUEST``
* ``SECURITY_MSG_CONFIRMATION_REQUIRED``
* ``SECURITY_MSG_CONFIRM_REGISTRATION``
* ``SECURITY_MSG_DISABLED_ACCOUNT``
* ``SECURITY_MSG_EMAIL_ALREADY_ASSOCIATED``
* ``SECURITY_MSG_EMAIL_CONFIRMED``
* ``SECURITY_MSG_EMAIL_NOT_PROVIDED``
* ``SECURITY_MSG_INVALID_CONFIRMATION_TOKEN``
* ``SECURITY_MSG_INVALID_EMAIL_ADDRESS``
* ``SECURITY_MSG_INVALID_LOGIN_TOKEN``
* ``SECURITY_MSG_INVALID_PASSWORD``
* ``SECURITY_MSG_INVALID_REDIRECT``
* ``SECURITY_MSG_INVALID_RESET_PASSWORD_TOKEN``
* ``SECURITY_MSG_LOGIN``
* ``SECURITY_MSG_LOGIN_EMAIL_SENT``
* ``SECURITY_MSG_LOGIN_EXPIRED``
* ``SECURITY_MSG_PASSWORDLESS_LOGIN_SUCCESSFUL``
* ``SECURITY_MSG_PASSWORD_CHANGE``
* ``SECURITY_MSG_PASSWORD_INVALID_LENGTH``
* ``SECURITY_MSG_PASSWORD_IS_THE_SAME``
* ``SECURITY_MSG_PASSWORD_MISMATCH``
* ``SECURITY_MSG_PASSWORD_NOT_PROVIDED``
* ``SECURITY_MSG_PASSWORD_NOT_SET``
* ``SECURITY_MSG_PASSWORD_RESET``
* ``SECURITY_MSG_PASSWORD_RESET_EXPIRED``
* ``SECURITY_MSG_PASSWORD_RESET_REQUEST``
* ``SECURITY_MSG_REFRESH``
* ``SECURITY_MSG_RETYPE_PASSWORD_MISMATCH``
* ``SECURITY_MSG_UNAUTHORIZED``
* ``SECURITY_MSG_USER_DOES_NOT_EXIST``
+5 -5
View File
@@ -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
+2 -2
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
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
@@ -89,7 +89,7 @@ User Registration
Flask-Security comes packaged with a basic user registration view. This view is
very simple and new users need only supply an email address and their password.
This view can be overrided if your registration process requires more fields.
This view can be overridden if your registration process requires more fields.
Login Tracking
+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
`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``
+1 -1
View File
@@ -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
+5 -1
View File
@@ -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)
+8 -7
View File
@@ -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
View File
@@ -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):
+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>
<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
View File
@@ -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
View File
@@ -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,
+1 -1
View File
@@ -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 }}
+6
View File
@@ -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
+6 -4
View File
@@ -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")
+20 -5
View File
@@ -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
+11
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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)
+24 -5
View File
@@ -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
+1 -1
View File
@@ -1,5 +1,5 @@
[tox]
envlist = py26, py27, py33, pypy
envlist = py26, py27, py33, py34, pypy
[testenv]
deps =