diff --git a/.travis.yml b/.travis.yml index 558e09b..beba9be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ python: - "2.6" - "2.7" - "3.3" + - "3.4" - "pypy" install: diff --git a/CHANGES b/CHANGES index 1973ecd..2e4b8e8 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,18 @@ Flask-Security Changelog Here you can see the full list of changes between each Flask-Security release. +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 ------------- diff --git a/flask_security/core.py b/flask_security/core.py index e6341e2..7e922b4 100644 --- a/flask_security/core.py +++ b/flask_security/core.py @@ -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 diff --git a/flask_security/utils.py b/flask_security/utils.py index 5b20352..f519b3d 100644 --- a/flask_security/utils.py +++ b/flask_security/utils.py @@ -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()) @@ -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,7 +208,7 @@ 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) diff --git a/flask_security/views.py b/flask_security/views.py index 847c980..be80372 100644 --- a/flask_security/views.py +++ b/flask_security/views.py @@ -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, @@ -121,7 +123,7 @@ def register(): if not request.json: return redirect(get_post_register_redirect()) - return _render_json(form, True) + return _render_json(form, include_auth_token=True) if request.json: return _render_json(form) @@ -247,7 +249,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, diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 9a75ae0..6bc3c52 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -23,11 +23,13 @@ def test_view_configuration(client): response = client.get('/custom_login') assert b"

Login

" 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") diff --git a/tests/test_misc.py b/tests/test_misc.py index 8be2a7d..9b76457 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -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,19 @@ 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 + + +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 diff --git a/tests/test_recoverable.py b/tests/test_recoverable.py index ade31a0..de278e1 100644 --- a/tests/test_recoverable.py +++ b/tests/test_recoverable.py @@ -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) diff --git a/tox.ini b/tox.ini index 341e949..15fa2e1 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26, py27, py33, pypy +envlist = py26, py27, py33, py34, pypy [testenv] deps =