From 19e234d1dc3cfde37e3494faad4fd1ad29973cd3 Mon Sep 17 00:00:00 2001 From: Jon Gregorowicz Date: Mon, 4 Jan 2016 12:45:33 -0500 Subject: [PATCH] * Added support for REST_USE_JWT * Added JWTSerializer * Added JWT encoding support, based on django-rest-framework-jwt * Tests for JWT authentication --- docs/configuration.rst | 4 +++- rest_auth/app_settings.py | 4 ++++ rest_auth/registration/views.py | 38 +++++++++++++++++++++++++------ rest_auth/serializers.py | 7 +++++- rest_auth/tests/requirements.pip | 1 + rest_auth/tests/settings.py | 11 ++++++++- rest_auth/tests/test_api.py | 30 ++++++++++++++++++++++++ rest_auth/tests/test_base.py | 5 +++- rest_auth/tests/test_social.py | 26 +++++++++++++++++++++ rest_auth/utils.py | 12 ++++++++++ rest_auth/views.py | 39 ++++++++++++++++++++++++-------- 11 files changed, 157 insertions(+), 20 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index 282f326..040c1cf 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -29,10 +29,12 @@ Configuration ... } - - **REST_SESSION_LOGIN** - Enable session login in Login API view (default: True) - **OLD_PASSWORD_FIELD_ENABLED** - set it to True if you want to have old password verification on password change enpoint (default: False) - **LOGOUT_ON_PASSWORD_CHANGE** - set to False if you want to keep the current user logged in after a password change + +- **REST_USE_JWT** - If enabled, this will use `django-rest-framework-jwt ` as a backend, and instead of session based tokens or Social Login keys, it will return a JWT. + diff --git a/rest_auth/app_settings.py b/rest_auth/app_settings.py index e0340b7..b52064f 100644 --- a/rest_auth/app_settings.py +++ b/rest_auth/app_settings.py @@ -2,6 +2,7 @@ from django.conf import settings from rest_auth.serializers import ( TokenSerializer as DefaultTokenSerializer, + JWTSerializer as DefaultJWTSerializer, UserDetailsSerializer as DefaultUserDetailsSerializer, LoginSerializer as DefaultLoginSerializer, PasswordResetSerializer as DefaultPasswordResetSerializer, @@ -15,6 +16,9 @@ serializers = getattr(settings, 'REST_AUTH_SERIALIZERS', {}) TokenSerializer = import_callable( serializers.get('TOKEN_SERIALIZER', DefaultTokenSerializer)) +JWTSerializer = import_callable( + serializers.get('JWT_SERIALIZER', DefaultJWTSerializer)) + UserDetailsSerializer = import_callable( serializers.get('USER_DETAILS_SERIALIZER', DefaultUserDetailsSerializer) ) diff --git a/rest_auth/registration/views.py b/rest_auth/registration/views.py index e700706..ce68a86 100644 --- a/rest_auth/registration/views.py +++ b/rest_auth/registration/views.py @@ -1,4 +1,6 @@ from django.http import HttpRequest +from django.conf import settings + from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import AllowAny @@ -13,6 +15,8 @@ from rest_auth.app_settings import TokenSerializer from rest_auth.registration.serializers import SocialLoginSerializer from rest_auth.views import LoginView +from rest_auth.utils import jwt_encode + class RegisterView(APIView, SignupView): """ @@ -28,7 +32,12 @@ class RegisterView(APIView, SignupView): permission_classes = (AllowAny,) allowed_methods = ('POST', 'OPTIONS', 'HEAD') token_model = Token - serializer_class = TokenSerializer + + def get_serializer_class(self): + if getattr(settings, 'REST_USE_JWT', False): + return JWTSerializer + else: + return TokenSerializer def get(self, *args, **kwargs): return Response({}, status=status.HTTP_405_METHOD_NOT_ALLOWED) @@ -38,9 +47,14 @@ class RegisterView(APIView, SignupView): def form_valid(self, form): self.user = form.save(self.request) - self.token, created = self.token_model.objects.get_or_create( - user=self.user - ) + + if getattr(settings, 'REST_USE_JWT', False): + self.token = jwt_encode(self.user) + + else: + self.token, created = self.token_model.objects.get_or_create( + user=self.user + ) if isinstance(self.request, HttpRequest): request = self.request else: @@ -65,9 +79,19 @@ class RegisterView(APIView, SignupView): return self.get_response_with_errors() def get_response(self): - # serializer = self.user_serializer_class(instance=self.user) - serializer = self.serializer_class(instance=self.token, - context={'request': self.request}) + serializer_class = self.get_serializer_class() + + if getattr(settings, 'REST_USE_JWT', False): + data = { + 'user': self.user, + 'token': self.token + } + serializer = serializer_class(instance=data, + context={'request': self.request}) + else: + serializer = serializer_class(instance=self.token, + context={'request': self.request}) + return Response(serializer.data, status=status.HTTP_201_CREATED) def get_response_with_errors(self): diff --git a/rest_auth/serializers.py b/rest_auth/serializers.py index a2d1a82..1145826 100644 --- a/rest_auth/serializers.py +++ b/rest_auth/serializers.py @@ -87,7 +87,6 @@ class TokenSerializer(serializers.ModelSerializer): model = Token fields = ('key',) - class UserDetailsSerializer(serializers.ModelSerializer): """ @@ -98,6 +97,12 @@ class UserDetailsSerializer(serializers.ModelSerializer): fields = ('username', 'email', 'first_name', 'last_name') read_only_fields = ('email', ) +class JWTSerializer(serializers.Serializer): + """ + Serializer for JWT authentication. + """ + token = serializers.CharField() + user = UserDetailsSerializer() class PasswordResetSerializer(serializers.Serializer): diff --git a/rest_auth/tests/requirements.pip b/rest_auth/tests/requirements.pip index bb8d844..4040970 100644 --- a/rest_auth/tests/requirements.pip +++ b/rest_auth/tests/requirements.pip @@ -1,3 +1,4 @@ django-allauth>=0.19.1 responses>=0.3.0 flake8==2.4.0 +djangorestframework-jwt>=1.7.2 \ No newline at end of file diff --git a/rest_auth/tests/settings.py b/rest_auth/tests/settings.py index b09b496..aab38d3 100644 --- a/rest_auth/tests/settings.py +++ b/rest_auth/tests/settings.py @@ -45,6 +45,13 @@ TEMPLATE_CONTEXT_PROCESSORS = [ "allauth.socialaccount.context_processors.socialaccount", ] +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework.authentication.SessionAuthentication', + 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', + ) +} + INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', @@ -64,7 +71,9 @@ INSTALLED_APPS = [ 'rest_framework.authtoken', 'rest_auth', - 'rest_auth.registration' + 'rest_auth.registration', + + 'rest_framework_jwt' ] SECRET_KEY = "38dh*skf8sjfhs287dh&^hd8&3hdg*j2&sd" diff --git a/rest_auth/tests/test_api.py b/rest_auth/tests/test_api.py index b64cf8c..3f844f9 100644 --- a/rest_auth/tests/test_api.py +++ b/rest_auth/tests/test_api.py @@ -90,6 +90,19 @@ class APITestCase1(TestCase, BaseAPITestCase): # test empty payload self.post(self.login_url, data={}, status_code=400) + @override_settings(REST_USE_JWT=True) + def test_login_jwt(self): + payload = { + "username": self.USERNAME, + "password": self.PASS + } + # no users in db so it should throw an error + user = get_user_model().objects.create_user(self.USERNAME, '', self.PASS) + self.post(self.login_url, data=payload, status_code=200) + self.assertEqual('user' in self.response.json.keys(), True) + self.assertEqual('token' in self.response.json.keys(), True) + + def test_password_change(self): login_payload = { "username": self.USERNAME, @@ -258,6 +271,23 @@ class APITestCase1(TestCase, BaseAPITestCase): self.assertEqual(user.last_name, self.response.json['last_name']) self.assertEqual(user.email, self.response.json['email']) + @override_settings(REST_USE_JWT=True) + def test_user_details_jwt(self): + user = get_user_model().objects.create_user(self.USERNAME, self.EMAIL, self.PASS) + payload = { + 'username': self.USERNAME, + 'password': self.PASS + } + self.post(self.login_url, data=payload, status_code=200) + self.token = self.response.json['token'] + self.get(self.user_url, status_code=200) + + self.patch(self.user_url, data=self.BASIC_USER_DATA, status_code=200) + user = get_user_model().objects.get(pk=user.pk) + self.assertEqual(user.first_name, self.response.json['first_name']) + self.assertEqual(user.last_name, self.response.json['last_name']) + self.assertEqual(user.email, self.response.json['email']) + def test_registration(self): user_count = get_user_model().objects.all().count() diff --git a/rest_auth/tests/test_base.py b/rest_auth/tests/test_base.py index ed8ffeb..97dfa31 100644 --- a/rest_auth/tests/test_base.py +++ b/rest_auth/tests/test_base.py @@ -37,7 +37,10 @@ class BaseAPITestCase(object): # check_headers = kwargs.pop('check_headers', True) if hasattr(self, 'token'): - kwargs['HTTP_AUTHORIZATION'] = 'Token %s' % self.token + if getattr(settings, 'REST_USE_JWT', False): + kwargs['HTTP_AUTHORIZATION'] = 'JWT %s' % self.token + else: + kwargs['HTTP_AUTHORIZATION'] = 'Token %s' % self.token self.response = request_func(*args, **kwargs) is_json = bool( diff --git a/rest_auth/tests/test_social.py b/rest_auth/tests/test_social.py index 0bb10f5..ad43b7d 100644 --- a/rest_auth/tests/test_social.py +++ b/rest_auth/tests/test_social.py @@ -126,3 +126,29 @@ class TestSocialAuth(TestCase, BaseAPITestCase): self.post(self.fb_login_url, data=payload, status_code=200) self.assertIn('key', self.response.json.keys()) + + @responses.activate + @override_settings( + REST_USE_JWT=True + ) + def test_jwt(self): + resp_body = '{"id":"123123123123","first_name":"John","gender":"male","last_name":"Smith","link":"https:\\/\\/www.facebook.com\\/john.smith","locale":"en_US","name":"John Smith","timezone":2,"updated_time":"2014-08-13T10:14:38+0000","username":"john.smith","verified":true}' # noqa + responses.add( + responses.GET, + self.graph_api_url, + body=resp_body, + status=200, + content_type='application/json' + ) + + users_count = get_user_model().objects.all().count() + payload = { + 'access_token': 'abc123' + } + + self.post(self.fb_login_url, data=payload, status_code=200) + self.assertIn('token', self.response.json.keys()) + self.assertIn('user', self.response.json.keys()) + + self.assertEqual(get_user_model().objects.all().count(), users_count + 1) + diff --git a/rest_auth/utils.py b/rest_auth/utils.py index a32da60..8b6e8c1 100644 --- a/rest_auth/utils.py +++ b/rest_auth/utils.py @@ -9,3 +9,15 @@ def import_callable(path_or_callable): assert isinstance(path_or_callable, string_types) package, attr = path_or_callable.rsplit('.', 1) return getattr(import_module(package), attr) + +def jwt_encode(user): + try: + from rest_framework_jwt.settings import api_settings + except ImportError: + raise ImportError('rest_framework_jwt needs to be installed') + + jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER + jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER + + payload = jwt_payload_handler(user) + return jwt_encode_handler(payload) diff --git a/rest_auth/views.py b/rest_auth/views.py index 3af1557..16c6439 100644 --- a/rest_auth/views.py +++ b/rest_auth/views.py @@ -13,9 +13,11 @@ from rest_framework.generics import RetrieveUpdateAPIView from .app_settings import ( TokenSerializer, UserDetailsSerializer, LoginSerializer, PasswordResetSerializer, PasswordResetConfirmSerializer, - PasswordChangeSerializer + PasswordChangeSerializer, JWTSerializer ) +from .utils import jwt_encode + class LoginView(GenericAPIView): @@ -31,19 +33,38 @@ class LoginView(GenericAPIView): permission_classes = (AllowAny,) serializer_class = LoginSerializer token_model = Token - response_serializer = TokenSerializer + + def get_response_serializer(self): + if getattr(settings, 'REST_USE_JWT', False): + response_serializer = JWTSerializer + else: + response_serializer = TokenSerializer + return response_serializer def login(self): self.user = self.serializer.validated_data['user'] - self.token, created = self.token_model.objects.get_or_create( - user=self.user) - if getattr(settings, 'REST_SESSION_LOGIN', True): - login(self.request, self.user) + + if getattr(settings, 'REST_USE_JWT', False): + self.token = jwt_encode(self.user) + else: + self.token, created = self.token_model.objects.get_or_create( + user=self.user) + if getattr(settings, 'REST_SESSION_LOGIN', True): + login(self.request, self.user) def get_response(self): - return Response( - self.response_serializer(self.token).data, status=status.HTTP_200_OK - ) + serializer_class = self.get_response_serializer() + + if getattr(settings, 'REST_USE_JWT', False): + data = { + 'user': self.user, + 'token': self.token + } + serializer = serializer_class(instance=data) + else: + serializer = serializer_class(instance=self.token) + + return Response(serializer.data, status=status.HTTP_200_OK) def post(self, request, *args, **kwargs): self.serializer = self.get_serializer(data=self.request.data)