From 872877fb4185004031a8e5023b6d4d6d7d845123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Rychlik?= Date: Mon, 5 May 2014 17:38:49 +0200 Subject: [PATCH] 0.1.6 - JWT refactoring including custom django middleware and auth backend --- README.rst | 2 +- jsonis/jwt/__init__.py | 1 + jsonis/jwt/auth_backends.py | 18 ++++++++++++++++ jsonis/jwt/middleware.py | 37 +++++++++++++++++++++++++++++++++ jsonis/jwt/utils.py | 29 ++++++++++++++++++++++++++ jsonis/{jwt.py => jwt/views.py} | 28 ++++++++++--------------- setup.py | 6 +++--- 7 files changed, 100 insertions(+), 21 deletions(-) create mode 100644 jsonis/jwt/__init__.py create mode 100644 jsonis/jwt/auth_backends.py create mode 100644 jsonis/jwt/middleware.py create mode 100644 jsonis/jwt/utils.py rename jsonis/{jwt.py => jwt/views.py} (61%) diff --git a/README.rst b/README.rst index feae628..59b49ad 100644 --- a/README.rst +++ b/README.rst @@ -10,7 +10,7 @@ Features - JSONApiResponseMixin providing render_api_response and render_api_error_response methods - Accept JSON request payload - JSONApiFormView that can be used for simple JSON APIs using django.forms -- JSON Web token validation (pending to be moved into separate package) +- JSON Web token validation (custom django middleware&auth backend) - JSONTestClient providing utilities that can be used to test created APIs - Default JSONEncoder handling Decimal numbers - Simple jsonify template filter (add 'jsonis' to your application list in settings) diff --git a/jsonis/jwt/__init__.py b/jsonis/jwt/__init__.py new file mode 100644 index 0000000..aef3a98 --- /dev/null +++ b/jsonis/jwt/__init__.py @@ -0,0 +1 @@ +from views import JWTAuthorizationMixin diff --git a/jsonis/jwt/auth_backends.py b/jsonis/jwt/auth_backends.py new file mode 100644 index 0000000..c2853e9 --- /dev/null +++ b/jsonis/jwt/auth_backends.py @@ -0,0 +1,18 @@ +from django.contrib.auth import get_user_model +from django.contrib.auth.backends import ModelBackend + +from .utils import parse_token, JWTParseError + +class JWTAuthenticationBackend(ModelBackend): + """Custom django authentication backend using JWT""" + def authenticate(self, authorization_token=None, **kwargs): + UserModel = get_user_model() + if authorization_token is None: + return + try: + token_data = parse_token(authorization_token) + return UserModel._default_manager.get(pk=token_data['id']) + except JWTParseError: + pass + except UserModel.DoesNotExist: + pass \ No newline at end of file diff --git a/jsonis/jwt/middleware.py b/jsonis/jwt/middleware.py new file mode 100644 index 0000000..db634c3 --- /dev/null +++ b/jsonis/jwt/middleware.py @@ -0,0 +1,37 @@ +from django.contrib.auth import authenticate, login, get_user_model +from django.core.exceptions import ImproperlyConfigured + +class JWTAuthMiddleware(object): + """Authentication Middleware that checks for a JSON Web Token in the Authorization header + + JWTAuthenticationBackend needs to be added to AUTHENTICATION_BACKENDS as well""" + + # Used HTTP Header + header = 'HTTP_AUTHORIZATION' + + # Required header prefix + required_auth_prefix = 'Bearer' + + def process_request(self, request): + if not hasattr(request, 'user'): + raise ImproperlyConfigured( + "The JWT auth middleware requires the authentication middleware to be installed. Edit your" + " MIDDLEWARE_CLASSES setting to insert 'django.contrib.auth.middleware.AuthenticationMiddleware'" + " before the JWTAuthMiddleware class.") + + try: + auth_prefix, auth_token = request.META[self.header].split(' ') + if auth_prefix != self.required_auth_prefix: + raise ValueError + + user = authenticate(authorization_token=auth_token) + if user: + request.user = user + login(request, user) + + except KeyError: + # There is no self.header + pass + except ValueError: + # Header prefix doesn't match + pass diff --git a/jsonis/jwt/utils.py b/jsonis/jwt/utils.py new file mode 100644 index 0000000..6b3d850 --- /dev/null +++ b/jsonis/jwt/utils.py @@ -0,0 +1,29 @@ +import jwt + +from django.conf import settings + +class JWTParseError(Exception): + pass + +class JWTDecodeError(JWTParseError): + pass + +class JWTExpiredError(JWTParseError): + pass + +class JWTNoDataError(JWTDecodeError): + pass + +def parse_token(auth_token): + """Parser given JSON Web Token and returns contained data""" + try: + decoded_token = jwt.decode(auth_token, settings.FIREBASE_SECRET) + except (jwt.DecodeError, ValueError): + raise JWTDecodeError('Decoding of authorization token failed') + except jwt.ExpiredSignature: + raise JWTExpiredError('Expired authorization token') + + try: + return decoded_token['d'] + except (TypeError, KeyError): + raise JWTNoDataError('No data in authorization token') diff --git a/jsonis/jwt.py b/jsonis/jwt/views.py similarity index 61% rename from jsonis/jwt.py rename to jsonis/jwt/views.py index 0e08d91..bab1fae 100644 --- a/jsonis/jwt.py +++ b/jsonis/jwt/views.py @@ -1,17 +1,16 @@ -from __future__ import absolute_import - -import jwt - -from django.conf import settings from django.contrib.auth import get_user_model from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt -from .views import JSONApiResponseMixin +from jsonis.views import JSONApiResponseMixin +from .utils import parse_token, JWTParseError class JWTAuthorizationMixin(JSONApiResponseMixin): - """JSON API mixin that enables JSON Web Token authorization""" + """JSON API mixin that enables JSON Web Token authorization + + FIXME: Deprecated, move to middleware & auth backend""" + @method_decorator(csrf_exempt) def dispatch(self, request, *args, **kwargs): if request.user.is_authenticated(): @@ -20,20 +19,15 @@ def dispatch(self, request, *args, **kwargs): try: auth_prefix, auth_token = request.META['HTTP_AUTHORIZATION'].split(' ') if auth_prefix != 'Bearer': - raise ValueError - token_data = jwt.decode(auth_token, settings.FIREBASE_SECRET) + return self.render_api_error_response('Not authenticated - Bad authorization header', status=401) + token_data = parse_token(auth_token) except KeyError: return self.render_api_error_response('Not authenticated - Missing authorization header', status=401) - except ValueError: - return self.render_api_error_response('Not authenticated - Bad authorization header', status=401) - except jwt.DecodeError: - return self.render_api_error_response('Not authenticated - Bad authorization header (decode failed)', - status=401) - except jwt.ExpiredSignature: - return self.render_api_error_response('Not authenticated - Expired authorization header', status=401) + except JWTParseError as e: + return self.render_api_error_response('Not authenticated - %s' % e, status=401) try: - self.user = get_user_model().objects.get(pk=token_data['d']['id']) + self.user = get_user_model().objects.get(pk=token_data['id']) except (TypeError, KeyError): return self.render_api_error_response('Not authenticated - Bad authorization header data', status=401) except get_user_model().DoesNotExist: diff --git a/setup.py b/setup.py index 98b1e8c..98be15c 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,14 @@ #!/usr/bin/env python -from distutils.core import setup +from setuptools import setup setup( name='django-jsonis', - version='0.1.5', + version='0.1.6', description='Django JSON Utils', author='Tomas Rychlik', author_email='rychlis@rychlis.cz', - packages=['jsonis', 'jsonis.templatetags'], + packages=['jsonis', 'jsonis.templatetags', 'jsonis.jwt'], license='MIT', url='https://github.com/rychlis/django-jsonis', install_requires=[