diff --git a/docs/changelog.rst b/docs/changelog.rst index e3e9064..a24d7f5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,10 @@ Changelog ========= +0.8.0 +----- +- added support for django-rest-framework-jwt + 0.7.0 ----- - Wrapped API returned strings in ugettext_lazy diff --git a/docs/configuration.rst b/docs/configuration.rst index e00d2b5..0728e11 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -10,6 +10,8 @@ Configuration - TOKEN_SERIALIZER - response for successful authentication in ``rest_auth.views.LoginView``, default value ``rest_auth.serializers.TokenSerializer`` + - JWT_SERIALIZER - (Using REST_USE_JWT=True) response for successful authentication in ``rest_auth.views.LoginView``, default value ``rest_auth.serializers.JWTSerializer`` + - USER_DETAILS_SERIALIZER - serializer class in ``rest_auth.views.UserDetailsView``, default value ``rest_auth.serializers.UserDetailsSerializer`` - PASSWORD_RESET_SERIALIZER - serializer class in ``rest_auth.views.PasswordResetView``, default value ``rest_auth.serializers.PasswordResetSerializer`` @@ -42,6 +44,8 @@ Configuration - **REST_SESSION_LOGIN** - Enable session login in Login API view (default: True) +- **REST_USE_JWT** - Enable JWT Authentication instead of Token/Session based. This is built on top of django-rest-framework-jwt http://getblimp.github.io/django-rest-framework-jwt/ , which much also be installed. (default: False) + - **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 diff --git a/docs/installation.rst b/docs/installation.rst index 60b47ca..9e47a47 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -140,3 +140,18 @@ If you are using Twitter for your social authentication, it is a bit different s url(r'^rest-auth/twitter/$', TwitterLogin.as_view(), name='twitter_login') ) .. note:: Starting from v0.21.0, django-allauth has dropped support for context processors. Check out http://django-allauth.readthedocs.org/en/latest/changelog.html#from-0-21-0 for more details. + + +JWT Support (optional) +---------------------- + +By default, ``django-rest-auth`` uses Django's Token-based authentication. If you want to use JWT authentication, you need to install the following: + +1. Install ``django-rest-framework-jwt`` http://getblimp.github.io/django-rest-framework-jwt/ . Right now this is the only supported JWT library. + +2. Add the following to your settings + +.. code-block:: python + + REST_USE_JWT = True + diff --git a/rest_auth/app_settings.py b/rest_auth/app_settings.py index b77d1d2..1b75fe6 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, @@ -17,6 +18,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 1f9da3c..d1d8d37 100644 --- a/rest_auth/registration/views.py +++ b/rest_auth/registration/views.py @@ -1,4 +1,5 @@ from django.utils.translation import ugettext_lazy as _ +from django.conf import settings from rest_framework.views import APIView from rest_framework.response import Response @@ -12,6 +13,7 @@ from allauth.account.utils import complete_signup from allauth.account import app_settings as allauth_settings from rest_auth.app_settings import (TokenSerializer, + JWTSerializer, create_token) from rest_auth.registration.serializers import (SocialLoginSerializer, VerifyEmailSerializer) @@ -19,6 +21,7 @@ from rest_auth.views import LoginView from rest_auth.models import TokenModel from .app_settings import RegisterSerializer +from rest_auth.utils import jwt_encode class RegisterView(CreateAPIView): serializer_class = RegisterSerializer @@ -30,7 +33,14 @@ class RegisterView(CreateAPIView): allauth_settings.EmailVerificationMethod.MANDATORY: return {} - return TokenSerializer(user.auth_token).data + if getattr(settings, 'REST_USE_JWT', False): + data = { + 'user': user, + 'token': self.token + } + return JWTSerializer(data).data + else: + return TokenSerializer(user.auth_token).data def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) @@ -42,7 +52,10 @@ class RegisterView(CreateAPIView): def perform_create(self, serializer): user = serializer.save(self.request) - create_token(self.token_model, user, serializer) + if getattr(settings, 'REST_USE_JWT', False): + self.token = jwt_encode(user) + else: + create_token(self.token_model, user, serializer) complete_signup(self.request._request, user, allauth_settings.EMAIL_VERIFICATION, None) diff --git a/rest_auth/serializers.py b/rest_auth/serializers.py index 86f9007..a414d23 100644 --- a/rest_auth/serializers.py +++ b/rest_auth/serializers.py @@ -118,7 +118,6 @@ class TokenSerializer(serializers.ModelSerializer): model = TokenModel fields = ('key',) - class UserDetailsSerializer(serializers.ModelSerializer): """ @@ -129,6 +128,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..de66892 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 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 81e7028..f75322c 100644 --- a/rest_auth/tests/test_api.py +++ b/rest_auth/tests/test_api.py @@ -91,6 +91,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 + } + user = get_user_model().objects.create_user(self.USERNAME, '', self.PASS) + + self.post(self.login_url, data=payload, status_code=200) + self.assertEqual('token' in self.response.json.keys(), True) + self.token = self.response.json['token'] + + def test_login_by_email(self): # starting test without allauth app settings.INSTALLED_APPS.remove('allauth') @@ -307,6 +320,22 @@ 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_using_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.email, self.response.json['email']) + + def test_registration(self): user_count = get_user_model().objects.all().count() @@ -323,6 +352,20 @@ class APITestCase1(TestCase, BaseAPITestCase): self._login() self._logout() + @override_settings(REST_USE_JWT=True) + def test_registration_with_jwt(self): + user_count = get_user_model().objects.all().count() + + self.post(self.register_url, data={}, status_code=400) + + result = self.post(self.register_url, data=self.REGISTRATION_DATA, status_code=201) + self.assertIn('token', result.data) + self.assertEqual(get_user_model().objects.all().count(), user_count + 1) + + self._login() + self._logout() + + def test_registration_with_invalid_password(self): data = self.REGISTRATION_DATA.copy() data['password2'] = 'foobar' 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 19509ef..34c912e 100644 --- a/rest_auth/tests/test_social.py +++ b/rest_auth/tests/test_social.py @@ -125,3 +125,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 e224f26..5cb94d7 100644 --- a/rest_auth/utils.py +++ b/rest_auth/utils.py @@ -14,3 +14,15 @@ def import_callable(path_or_callable): def default_create_token(token_model, user, serializer): token, _ = token_model.objects.get_or_create(user=user) return token + +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 1e016f6..25f47f2 100644 --- a/rest_auth/views.py +++ b/rest_auth/views.py @@ -15,10 +15,12 @@ from allauth.account import app_settings as allauth_settings from .app_settings import ( TokenSerializer, UserDetailsSerializer, LoginSerializer, PasswordResetSerializer, PasswordResetConfirmSerializer, - PasswordChangeSerializer, create_token + PasswordChangeSerializer, JWTSerializer, create_token ) from .models import TokenModel +from .utils import jwt_encode + class LoginView(GenericAPIView): @@ -34,18 +36,39 @@ class LoginView(GenericAPIView): permission_classes = (AllowAny,) serializer_class = LoginSerializer token_model = TokenModel - 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 = create_token(self.token_model, self.user, self.serializer) + + if getattr(settings, 'REST_USE_JWT', False): + self.token = jwt_encode(self.user) + else: + self.token = create_token(self.token_model, self.user, self.serializer) + 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) diff --git a/setup.py b/setup.py index 49deb9a..9b33b7f 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ f.close() setup( name='django-rest-auth', - version='0.7.0', + version='0.8.0', author='Sumit Chachra', author_email='chachra@tivix.com', url='http://github.com/Tivix/django-rest-auth',