Changed token auth a bit, including the use of itsdangerous. Also added JSON authentication feature

This commit is contained in:
Matt Wright
2012-07-11 16:31:21 -04:00
parent b3de4a76d5
commit 5e1d18c9e8
9 changed files with 101 additions and 58 deletions
+1 -2
View File
@@ -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():
-10
View File
@@ -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.
+1 -1
View File
@@ -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
+16 -2
View File
@@ -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 = """
<h1>Unauthorized</h1>
<p>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
+9 -35
View File
@@ -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)
+2 -1
View File
@@ -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
+39 -4
View File
@@ -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)
+11
View File
@@ -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)
+22 -3
View File
@@ -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):