From 5e1d18c9e81e0dd8ca58d09eef4838c260c1ac51 Mon Sep 17 00:00:00 2001 From: Matt Wright Date: Wed, 11 Jul 2012 16:29:52 -0400 Subject: [PATCH] Changed token auth a bit, including the use of itsdangerous. Also added JSON authentication feature --- example/app.py | 3 +-- flask_security/confirmable.py | 10 -------- flask_security/core.py | 2 +- flask_security/decorators.py | 18 ++++++++++++-- flask_security/tokens.py | 44 +++++++---------------------------- flask_security/utils.py | 3 ++- flask_security/views.py | 43 ++++++++++++++++++++++++++++++---- tests/__init__.py | 11 +++++++++ tests/functional_tests.py | 25 +++++++++++++++++--- 9 files changed, 101 insertions(+), 58 deletions(-) diff --git a/example/app.py b/example/app.py index bbe1b4a..ededb72 100644 --- a/example/app.py +++ b/example/app.py @@ -32,8 +32,7 @@ def create_users(): ('jill@lp.com', 'password', ['author'], True), ('tiya@lp.com', 'password', [], False)): current_app.security.datastore.create_user( - email=u[0], password=u[1], roles=u[2], active=u[3], - authentication_token='123abc') + email=u[0], password=u[1], roles=u[2], active=u[3]) def populate_data(): diff --git a/flask_security/confirmable.py b/flask_security/confirmable.py index e86e16e..c1f981b 100644 --- a/flask_security/confirmable.py +++ b/flask_security/confirmable.py @@ -26,16 +26,6 @@ _security = LocalProxy(lambda: app.security) _datastore = LocalProxy(lambda: app.security.datastore) -def find_user_by_confirmation_token(token): - """Returns a user with a matching confirmation token. - - :param token: The reset password token - """ - if not token: - raise ConfirmationError('Confirmation token required') - return _datastore.find_user(confirmation_token=token) - - def send_confirmation_instructions(user, token): """Sends the confirmation instructions email for the specified user. diff --git a/flask_security/core.py b/flask_security/core.py index 9044bc3..f5148d2 100644 --- a/flask_security/core.py +++ b/flask_security/core.py @@ -19,7 +19,7 @@ from passlib.context import CryptContext from werkzeug.datastructures import ImmutableList from . import views, exceptions -from .confirmable import requires_confirmation, reset_confirmation_token +from .confirmable import requires_confirmation from .decorators import login_required from .utils import config_value as cv, get_config diff --git a/flask_security/decorators.py b/flask_security/decorators.py index 455a3d3..b03d9f8 100644 --- a/flask_security/decorators.py +++ b/flask_security/decorators.py @@ -14,10 +14,15 @@ from functools import wraps from flask import current_app as app, Response, request, abort, redirect from flask.ext.login import login_required, login_url, current_user from flask.ext.principal import RoleNeed, Permission +from werkzeug.local import LocalProxy from . import utils +# Convenient references +_security = LocalProxy(lambda: app.security) + + _default_http_auth_msg = """

Unauthorized

The server could not verify that you are authorized to access the URL @@ -29,15 +34,24 @@ _default_http_auth_msg = """ def _check_token(): + header_key = app.security.token_authentication_header args_key = app.security.token_authentication_key header_token = request.headers.get(header_key, None) token = request.args.get(args_key, header_token) + serializer = _security.token_auth_serializer + try: - app.security.datastore.find_user(authentication_token=token) - except: + data = serializer.loads(token) + user = app.security.datastore.find_user(id=data[0], + authentication_token=token) + + if data[1] != utils.md5(user.email): + raise Exception() + + except Exception: return False return True diff --git a/flask_security/tokens.py b/flask_security/tokens.py index 081bf2a..f6665f0 100644 --- a/flask_security/tokens.py +++ b/flask_security/tokens.py @@ -9,51 +9,25 @@ :license: MIT, see LICENSE for more details. """ -from datetime import datetime - from flask import current_app as app from werkzeug.local import LocalProxy -from .exceptions import BadCredentialsError, UserNotFoundError -from .utils import generate_token +from .utils import md5 # Convenient references +_security = LocalProxy(lambda: app.security) + _datastore = LocalProxy(lambda: app.security.datastore) -def find_user_by_authentication_token(token): - """Returns a user with a matching authentication token. - - :param token: The authentication token - """ - if not token: - raise BadCredentialsError('Authentication token required') - return _datastore.find_user(authentication_token=token) - - def generate_authentication_token(user): """Generates a unique authentication token for the specified user. :param user: The user to work with """ - while True: - token = generate_token() - try: - find_user_by_authentication_token(token) - except UserNotFoundError: - break - - now = datetime.utcnow() - - try: - user['authentication_token'] = token - user['authentication_token_created_at'] = now - except TypeError: - user.authentication_token = token - user.authentication_token_created_at = now - - return user + data = [str(user.id), md5(user.email)] + return _security.token_auth_serializer.dumps(data) def reset_authentication_token(user): @@ -61,9 +35,10 @@ def reset_authentication_token(user): :param user: The user to work with """ - user = generate_authentication_token(user) + token = generate_authentication_token(user) + user.authentication_token = token _datastore._save_model(user) - return user.authentication_token + return token def ensure_authentication_token(user): @@ -73,5 +48,4 @@ def ensure_authentication_token(user): :param user: The user to work with """ if not user.authentication_token: - reset_authentication_token(user) - return user.authentication_token + return reset_authentication_token(user) diff --git a/flask_security/utils.py b/flask_security/utils.py index 3e43e74..662563e 100644 --- a/flask_security/utils.py +++ b/flask_security/utils.py @@ -15,7 +15,8 @@ import os from contextlib import contextmanager from datetime import datetime, timedelta -from flask import url_for, flash, current_app, request, session, render_template +from flask import url_for, flash, current_app, request, session, \ + render_template from flask.ext.login import make_secure_token from .signals import user_registered, password_reset_requested diff --git a/flask_security/views.py b/flask_security/views.py index bbbd338..2e35236 100644 --- a/flask_security/views.py +++ b/flask_security/views.py @@ -12,9 +12,10 @@ from datetime import datetime from flask import current_app as app, redirect, request, session, \ - render_template + render_template, jsonify from flask.ext.login import login_user, logout_user from flask.ext.principal import Identity, AnonymousIdentity, identity_changed +from werkzeug.datastructures import MultiDict from werkzeug.local import LocalProxy from .confirmable import confirm_by_token, reset_confirmation_token @@ -25,6 +26,7 @@ from .forms import LoginForm, RegisterForm, ForgotPasswordForm, \ from .recoverable import reset_by_token, \ reset_password_reset_token from .signals import user_registered +from .tokens import generate_authentication_token from .utils import get_post_login_redirect, do_flash, get_remember_token @@ -42,6 +44,9 @@ def _do_login(user, remember=True): if not login_user(user, remember): return False + if user.authentication_token is None: + user.authentication_token = generate_authentication_token(user) + if remember: user.remember_token = get_remember_token(user.email, user.password) @@ -65,15 +70,45 @@ def _do_login(user, remember=True): return True +def _json_auth_ok(user): + return jsonify({ + "meta": { + "code": 200 + }, + "response": { + "user": { + "id": str(user.id), + "authentication_token": user.authentication_token + } + } + }) + + +def _json_auth_error(msg): + resp = jsonify({ + "meta": { + "code": 400 + }, + "response": { + "error": msg + } + }) + resp.status_code = 400 + return resp + + def authenticate(): """View function which handles an authentication request.""" - form = LoginForm() + form = LoginForm(MultiDict(request.json) if request.json else request.form) try: user = _security.auth_provider.authenticate(form) if _do_login(user, remember=form.remember.data): + if request.json: + return _json_auth_ok(user) + return redirect(get_post_login_redirect()) raise BadCredentialsError('Inactive user') @@ -81,8 +116,8 @@ def authenticate(): except BadCredentialsError, e: msg = str(e) - except Exception, e: - msg = 'Unknown authentication error' + if request.json: + return _json_auth_error(msg) do_flash(msg, 'error') _logger.debug('Unsuccessful authentication attempt: %s' % msg) diff --git a/tests/__init__.py b/tests/__init__.py index 7af88b9..09bbf93 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -36,6 +36,17 @@ class SecurityTest(TestCase): data = dict(email=email, password=password) return self._post(endpoint or '/auth', data=data) + def json_authenticate(self, email="matt@lp.com", password="password", endpoint=None): + data = """ +{ + "email": "%s", + "password": "%s" +} +""" + return self._post(endpoint or '/auth', + content_type="application/json", + data=data % (email, password)) + def logout(self, endpoint=None): return self._get(endpoint or '/logout', follow_redirects=True) diff --git a/tests/functional_tests.py b/tests/functional_tests.py index 634f2ff..5f6b6be 100644 --- a/tests/functional_tests.py +++ b/tests/functional_tests.py @@ -3,7 +3,11 @@ from __future__ import with_statement import time -from datetime import datetime, timedelta + +try: + import simplejson as json +except ImportError: + import json from flask.ext.security.utils import capture_registrations, \ capture_reset_password_requests @@ -95,12 +99,27 @@ class DefaultSecurityTests(SecurityTest): r = self._get("/admin_and_editor") self.assertIn('Admin and Editor Page', r.data) + def test_ok_json_auth(self): + r = self.json_authenticate() + self.assertIn('"code": 200', r.data) + + def test_invalid_json_auth(self): + r = self.json_authenticate(password='junk') + self.assertIn('"code": 400', r.data) + def test_token_auth_via_querystring_valid_token(self): - r = self._get('/token?auth_token=123abc') + r = self.json_authenticate() + data = json.loads(r.data) + token = data['response']['user']['authentication_token'] + r = self._get('/token?auth_token=' + token) self.assertIn('Token Authentication', r.data) def test_token_auth_via_header_valid_token(self): - r = self._get('/token', headers={"X-Auth-Token": '123abc'}) + r = self.json_authenticate() + data = json.loads(r.data) + token = data['response']['user']['authentication_token'] + headers = {"X-Auth-Token": token} + r = self._get('/token', headers=headers) self.assertIn('Token Authentication', r.data) def test_token_auth_via_querystring_invalid_token(self):