diff --git a/.gitignore b/.gitignore index 7d8d699..4b34032 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,8 @@ db.sqlite3 # IntelliJ IDE files .idea + +# Virtual Environments +.vagrant/ +cookbooks/ +tmp/ diff --git a/docs/api_endpoints.rst b/docs/api_endpoints.rst index 46d2a05..c87670b 100644 --- a/docs/api_endpoints.rst +++ b/docs/api_endpoints.rst @@ -4,77 +4,139 @@ API endpoints Basic ----- -- /rest-auth/login/ (POST) +Typically, auth data is sent in the body, with a header ``Content-Type: application/x-www-form-urlencoded`` - - username - - email - - password +/rest-auth/login/ (POST) +************************ +**Request (standalone):** - Returns Token key +- ``username`` or ``email`` (email will be used to lookup username) +- ``password`` (required) -- /rest-auth/logout/ (POST) +**Request (using django-allauth):** - .. note:: ``ACCOUNT_LOGOUT_ON_GET = True`` to allow logout using GET - this is the exact same configuration from allauth. NOT recommended, see: http://django-allauth.readthedocs.io/en/latest/views.html#logout +- ``username`` (required when ``ACCOUNT_AUTHENTICATION_METHOD = 'username'`` or ``'username_email'``) +- ``email`` (required when ``ACCOUNT_AUTHENTICATION_METHOD = 'email'`` or ``'username_email'``) +- ``password`` (required) -- /rest-auth/password/reset/ (POST) +**Response:** - - email +- ``token`` +- ``user`` (when using django-rest-framework-jwt or django-rest-knox) -- /rest-auth/password/reset/confirm/ (POST) +/rest-auth/logout/ (POST) +************************* + +**Request (standalone):** - - uid - - token - - new_password1 - - new_password2 +- No values expected - .. note:: uid and token are sent in email after calling /rest-auth/password/reset/ +**Request (using django-rest-knox):** -- /rest-auth/password/change/ (POST) +- ``Authorization: Token TOKEN`` *(Header)* - - new_password1 - - new_password2 - - old_password +**Response:** - .. note:: ``OLD_PASSWORD_FIELD_ENABLED = True`` to use old_password. - .. note:: ``LOGOUT_ON_PASSWORD_CHANGE = False`` to keep the user logged in after password change +- No values -- /rest-auth/user/ (GET, PUT, PATCH) +.. note:: ``ACCOUNT_LOGOUT_ON_GET = True`` to allow logout using GET - this is the exact same configuration from allauth. NOT recommended, see: http://django-allauth.readthedocs.io/en/latest/views.html#logout - - username - - first_name - - last_name +/rest-auth/logoutall/ (POST) +**************************** - Returns pk, username, email, first_name, last_name +This endpoint deletes all Knox tokens, and will only be loaded when `REST_USE_KNOX = True`. + +| **Request (using django-rest-knox):** + +- `Authorization`: `Token TOKEN` (Header) + +**Response:** + +- No values + +.. note:: ``ACCOUNT_LOGOUT_ON_GET = True`` to allow logout using GET - this is the exact same configuration from allauth. NOT recommended, see: http://django-allauth.readthedocs.io/en/latest/views.html#logout + +/rest-auth/password/reset/ (POST) +********************************* + +- email + +/rest-auth/password/reset/confirm/ (POST) +***************************************** + +- uid +- token +- new_password1 +- new_password2 + +.. note:: uid and token are sent in email after calling /rest-auth/password/reset/ + +/rest-auth/password/change/ (POST) +********************************** +- new_password1 +- new_password2 +- old_password + +.. note:: ``OLD_PASSWORD_FIELD_ENABLED = True`` to use old_password. +.. note:: ``LOGOUT_ON_PASSWORD_CHANGE = False`` to keep the user logged in after password change + +/rest-auth/user/ (GET, PUT, PATCH) +********************************** +- username +- first_name +- last_name + +Returns pk, username, email, first_name, last_name Registration ------------ -- /rest-auth/registration/ (POST) +/rest-auth/registration/ (POST) +******************************* - - username - - password1 - - password2 - - email +**Request (using django-allauth):** -- /rest-auth/registration/verify-email/ (POST) +- ``username`` (required when ``ACCOUNT_AUTHENTICATION_METHOD = 'username'`` or ``'username_email'``, or when ``ACCOUNT_USERNAME_REQUIRED = True``) +- ``email`` (required when ``ACCOUNT_AUTHENTICATION_METHOD = 'email'`` or ``'username_email'``, or when ``ACCOUNT_EMAIL_REQUIRED = True``) +- ``password1`` (required) +- ``password2`` (required) - - key +**Response (using django-allauth):** +- No values + +**Response (using django-allauth and django-rest-knox)** + +- ``token`` +- ``user`` + +/rest-auth/registration/verify-email/ (POST) +******************************************** + +**Request (using django-allauth):** + +- ``key`` + +**Response (using django-allauth):** + +- No values Social Media Authentication --------------------------- -Basing on example from installation section :doc:`Installation ` +Based on the example from the installation section :doc:`Installation ` -- /rest-auth/facebook/ (POST) +/rest-auth/facebook/ (POST) +*************************** - - access_token - - code +- ``access_token`` +- ``code`` .. note:: ``access_token`` OR ``code`` can be used as standalone arguments, see https://github.com/Tivix/django-rest-auth/blob/master/rest_auth/registration/views.py -- /rest-auth/twitter/ (POST) +/rest-auth/twitter/ (POST) +************************** - - access_token - - token_secret +- ``access_token`` +- ``token_secret`` diff --git a/docs/configuration.rst b/docs/configuration.rst index 1f5b40f..fdae9c4 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -12,6 +12,8 @@ Configuration - JWT_SERIALIZER - (Using REST_USE_JWT=True) response for successful authentication in ``rest_auth.views.LoginView``, default value ``rest_auth.serializers.JWTSerializer`` + - KNOX_SERIALIZER - (Using REST_USE_KNOX=True) response for successful authentication in ``rest_auth.views.LoginView`` and successful registration in ``rest_auth.registration.views.RegisterView`` (using ``django-allauth`` and ``ACCOUNT_EMAIL_VERIFICATION = 'optional' or 'none'``), default value ``rest_auth.serializers.KnoxSerializer`` + - 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`` @@ -48,6 +50,8 @@ Configuration - **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 must also be installed. (default: False) +- **REST_USE_KNOX** - Use Knox token authentication instead of the built-in Django-Rest-Framework token authentication. Knox makes some significant security improvements, and supports multiple tokens per user. https://github.com/James1345/django-rest-knox/ must be installed. Not compatible with ``REST_USE_JWT`` (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 +- **LOGOUT_ON_PASSWORD_CHANGE** - set to False if you want to keep the current user logged in after a password change (default: False) diff --git a/docs/installation.rst b/docs/installation.rst index 6144011..730a900 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -153,7 +153,7 @@ 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. +1. Install ``django-rest-framework-jwt`` http://getblimp.github.io/django-rest-framework-jwt/ . 2. Add the following to your settings @@ -161,3 +161,19 @@ By default, ``django-rest-auth`` uses Django's Token-based authentication. If yo REST_USE_JWT = True + +Knox (optional) +--------------- +By default, ``django-rest-auth`` uses Django's Token-based authentication. ``django-rest-knox`` provides more secure token authentication with additional features, including multiple tokens per user. + +Knox and JWT cannot currently be used simultaneously. + +1. Install ``django-rest-knox`` https://james1345.github.io/django-rest-knox/installation/ . + +2. Configure ``django-rest-knox`` https://james1345.github.io/django-rest-knox/settings/ . ``REST_KNOX['USER_SERIALIZER']`` will not be used. + +3. Add the following to your settings + +.. code-block:: python + + REST_USE_KNOX = True diff --git a/rest_auth/app_settings.py b/rest_auth/app_settings.py index 1b75fe6..3843665 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, + KnoxSerializer as DefaultKnoxSerializer, JWTSerializer as DefaultJWTSerializer, UserDetailsSerializer as DefaultUserDetailsSerializer, LoginSerializer as DefaultLoginSerializer, @@ -21,6 +22,9 @@ TokenSerializer = import_callable( JWTSerializer = import_callable( serializers.get('JWT_SERIALIZER', DefaultJWTSerializer)) +KnoxSerializer = import_callable( + serializers.get('KNOX_TOKEN_SERIALIZER', DefaultKnoxSerializer)) + UserDetailsSerializer = import_callable( serializers.get('USER_DETAILS_SERIALIZER', DefaultUserDetailsSerializer) ) diff --git a/rest_auth/registration/views.py b/rest_auth/registration/views.py index e35153d..c8a6d83 100644 --- a/rest_auth/registration/views.py +++ b/rest_auth/registration/views.py @@ -16,11 +16,12 @@ from allauth.account import app_settings as allauth_settings from rest_auth.app_settings import (TokenSerializer, JWTSerializer, + KnoxSerializer, create_token) from rest_auth.models import TokenModel from rest_auth.registration.serializers import (SocialLoginSerializer, VerifyEmailSerializer) -from rest_auth.utils import jwt_encode +from rest_auth.utils import create_knox_token, jwt_encode from rest_auth.views import LoginView from .app_settings import RegisterSerializer @@ -49,8 +50,14 @@ class RegisterView(CreateAPIView): 'token': self.token } return JWTSerializer(data).data + elif getattr(settings, 'REST_USE_KNOX', False): + data = { + 'user': user, + 'token': self.token + } + return KnoxSerializer(data).data else: - return TokenSerializer(user.auth_token).data + return TokenSerializer(self.token).data def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) @@ -65,9 +72,11 @@ class RegisterView(CreateAPIView): def perform_create(self, serializer): user = serializer.save(self.request) if getattr(settings, 'REST_USE_JWT', False): - self.token = jwt_encode(user) + self.token = jwt_encode(user) + elif getattr(settings, 'REST_USE_KNOX', False): + self.token = create_knox_token(user) else: - create_token(self.token_model, user, serializer) + self.token = create_token(self.token_model, user, serializer) complete_signup(self.request._request, user, allauth_settings.EMAIL_VERIFICATION, diff --git a/rest_auth/serializers.py b/rest_auth/serializers.py index d42242a..a1ddbc8 100644 --- a/rest_auth/serializers.py +++ b/rest_auth/serializers.py @@ -114,7 +114,6 @@ class TokenSerializer(serializers.ModelSerializer): """ Serializer for Token model. """ - class Meta: model = TokenModel fields = ('key',) @@ -133,7 +132,7 @@ class UserDetailsSerializer(serializers.ModelSerializer): # Required to allow using custom USER_DETAILS_SERIALIZER in # JWTSerializer. Defining it here to avoid circular imports rest_auth_serializers = getattr(settings, 'REST_AUTH_SERIALIZERS', {}) -JWTUserDetailsSerializer = import_callable( +CustomUserDetailsSerializer = import_callable( rest_auth_serializers.get('USER_DETAILS_SERIALIZER', UserDetailsSerializer) ) @@ -143,7 +142,15 @@ class JWTSerializer(serializers.Serializer): Serializer for JWT authentication. """ token = serializers.CharField() - user = JWTUserDetailsSerializer() + user = CustomUserDetailsSerializer() + + +class KnoxSerializer(serializers.Serializer): + """ + Serializer for Knox authentication. + """ + token = serializers.CharField() + user = CustomUserDetailsSerializer() class PasswordResetSerializer(serializers.Serializer): diff --git a/rest_auth/tests/requirements.pip b/rest_auth/tests/requirements.pip index de66892..eb10284 100644 --- a/rest_auth/tests/requirements.pip +++ b/rest_auth/tests/requirements.pip @@ -2,3 +2,4 @@ django-allauth>=0.19.1 responses>=0.3.0 flake8==2.4.0 djangorestframework-jwt>=1.7.2 +django-rest-knox>=2.3.0 diff --git a/rest_auth/tests/settings.py b/rest_auth/tests/settings.py index 060cc89..0ef7f6c 100644 --- a/rest_auth/tests/settings.py +++ b/rest_auth/tests/settings.py @@ -66,6 +66,15 @@ REST_FRAMEWORK = { ) } +REST_FRAMEWORK_KNOX = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'knox.auth.TokenAuthentication', + ), + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.IsAuthenticated', + ), +} + INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', @@ -88,7 +97,9 @@ INSTALLED_APPS = [ 'rest_auth', 'rest_auth.registration', - 'rest_framework_jwt' + 'rest_framework_jwt', + + 'knox' ] 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 a24e9f0..2b4e122 100644 --- a/rest_auth/tests/test_api.py +++ b/rest_auth/tests/test_api.py @@ -1,13 +1,23 @@ from django.core.urlresolvers import reverse from django.test import TestCase, override_settings + from django.contrib.auth import get_user_model from django.core import mail from django.conf import settings from django.utils.encoding import force_text +from django.utils.six.moves import reload_module from rest_framework import status +from rest_framework.test import APIClient +from rest_framework import views as drf_views + +from unittest.mock import patch + from allauth.account import app_settings as account_app_settings +from knox.models import AuthToken + from .test_base import BaseAPITestCase +from .settings import REST_FRAMEWORK_KNOX @override_settings(ROOT_URLCONF="tests.urls") @@ -148,6 +158,24 @@ class APITestCase1(TestCase, BaseAPITestCase): self.assertEqual('token' in self.response.json.keys(), True) self.token = self.response.json['token'] + @override_settings(REST_USE_KNOX=True, REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ('knox.auth.TokenAuthentication')},) + def test_login_knox(self): + payload = { + "username": self.USERNAME, + "password": self.PASS + } + 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'] + self.assertEqual('user' in self.response.json.keys(), True) + self.user = self.response.json['user'] + + self.post(self.login_url, data=payload, status_code=200) + self.assertNotEqual(self.token, self.response.json['token']) + def test_login_by_email(self): # starting test without allauth app settings.INSTALLED_APPS.remove('allauth') @@ -382,6 +410,21 @@ class APITestCase1(TestCase, BaseAPITestCase): user = get_user_model().objects.get(pk=user.pk) self.assertEqual(user.email, self.response.json['email']) + @override_settings(REST_USE_KNOX=True, REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ('knox.auth.TokenAuthentication')},) + def test_user_details_using_knox(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() @@ -411,6 +454,21 @@ class APITestCase1(TestCase, BaseAPITestCase): self._login() self._logout() + @override_settings(REST_USE_KNOX=True, REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ('knox.auth.TokenAuthentication')},) + def test_registration_with_knox(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.assertIn('user', 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' @@ -493,3 +551,43 @@ class APITestCase1(TestCase, BaseAPITestCase): self.post(self.login_url, data=payload, status_code=status.HTTP_200_OK) self.get(self.logout_url, status_code=status.HTTP_405_METHOD_NOT_ALLOWED) + + # @override_settings(REST_USE_KNOX=True, REST_FRAMEWORK = { + # 'DEFAULT_AUTHENTICATION_CLASSES': ('knox.auth.TokenAuthentication')},) + def test_logout_knox(self): + with override_settings(REST_USE_KNOX=True, REST_FRAMEWORK=REST_FRAMEWORK_KNOX): + reload_module(drf_views) + payload = { + "username": self.USERNAME, + "password": self.PASS + } + + get_user_model().objects.create_user(self.USERNAME, '', self.PASS) + + self.client = APIClient() + + response = self.client.post(self.login_url, data=payload, status_code=status.HTTP_200_OK) + self.client.credentials(HTTP_AUTHORIZATION=('Token %s' % response.data['token'])) + self.client.post(self.logout_url, status_code=status.HTTP_200_OK) + self.assertEqual(AuthToken.objects.count(), 0) + reload_module(drf_views) + + @override_settings(REST_USE_KNOX=True, REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ('knox.auth.TokenAuthentication')},) + def test_logout_all_knox(self): + payload = { + "username": self.USERNAME, + "password": self.PASS + } + + get_user_model().objects.create_user(self.USERNAME, '', self.PASS) + + self.logout_all_url = reverse('rest_logout_all') + + self.client = APIClient() + + self.client.post(self.login_url, data=payload, status_code=status.HTTP_200_OK) + response = self.client.post(self.login_url, data=payload, status_code=status.HTTP_200_OK) + self.client.credentials(HTTP_AUTHORIZATION=('Token %s' % response.data['token'])) + self.client.post(self.logout_all_url, status_code=status.HTTP_200_OK) + self.assertEqual(AuthToken.objects.count(), 0) diff --git a/rest_auth/tests/urls.py b/rest_auth/tests/urls.py index 6371218..e5f9d24 100644 --- a/rest_auth/tests/urls.py +++ b/rest_auth/tests/urls.py @@ -8,6 +8,7 @@ from allauth.socialaccount.providers.twitter.views import TwitterOAuthAdapter from rest_framework.decorators import api_view from rest_auth.urls import urlpatterns +from rest_auth.views import LogoutAllView from rest_auth.registration.views import SocialLoginView from rest_auth.social_serializers import TwitterLoginSerializer @@ -49,5 +50,6 @@ urlpatterns += [ url(r'^social-login/twitter/$', TwitterLogin.as_view(), name='tw_login'), url(r'^social-login/twitter-no-view/$', twitter_login_view, name='tw_login_no_view'), url(r'^social-login/twitter-no-adapter/$', TwitterLoginNoAdapter.as_view(), name='tw_login_no_adapter'), - url(r'^accounts/', include('allauth.socialaccount.urls')) + url(r'^accounts/', include('allauth.socialaccount.urls')), + url(r'^logoutall/$', LogoutAllView.as_view(), name='rest_logout_all'), ] diff --git a/rest_auth/urls.py b/rest_auth/urls.py index 7a35e9b..2d7b358 100644 --- a/rest_auth/urls.py +++ b/rest_auth/urls.py @@ -1,8 +1,9 @@ +from django.conf import settings from django.conf.urls import url from rest_auth.views import ( - LoginView, LogoutView, UserDetailsView, PasswordChangeView, - PasswordResetView, PasswordResetConfirmView + LoginView, LogoutView, LogoutAllView, UserDetailsView, + PasswordChangeView, PasswordResetView, PasswordResetConfirmView ) urlpatterns = [ @@ -18,3 +19,8 @@ urlpatterns = [ url(r'^password/change/$', PasswordChangeView.as_view(), name='rest_password_change'), ] + +if getattr(settings, 'REST_USE_KNOX', False): + urlpatterns.append( + url(r'^logoutall/$', LogoutAllView.as_view(), name='rest_logout_all') + ) diff --git a/rest_auth/utils.py b/rest_auth/utils.py index 800f184..bf147aa 100644 --- a/rest_auth/utils.py +++ b/rest_auth/utils.py @@ -16,6 +16,15 @@ def default_create_token(token_model, user, serializer): return token +def create_knox_token(user): + try: + from knox.models import AuthToken + except ImportError: + raise ImportError("django-rest-knox needs to be installed") + token = AuthToken.objects.create(user=user) + return token + + def jwt_encode(user): try: from rest_framework_jwt.settings import api_settings diff --git a/rest_auth/views.py b/rest_auth/views.py index 0493a76..3544edd 100644 --- a/rest_auth/views.py +++ b/rest_auth/views.py @@ -16,12 +16,19 @@ from rest_framework.generics import GenericAPIView, RetrieveUpdateAPIView from rest_framework.permissions import IsAuthenticated, AllowAny from .app_settings import ( - TokenSerializer, UserDetailsSerializer, LoginSerializer, - PasswordResetSerializer, PasswordResetConfirmSerializer, - PasswordChangeSerializer, JWTSerializer, create_token + TokenSerializer, KnoxSerializer, UserDetailsSerializer, + LoginSerializer, PasswordResetSerializer, + PasswordResetConfirmSerializer, PasswordChangeSerializer, + JWTSerializer, create_token ) from .models import TokenModel -from .utils import jwt_encode +from .utils import create_knox_token, jwt_encode + +if getattr(settings, 'REST_USE_KNOX', False): + try: + from knox.auth import TokenAuthentication as KnoxTokenAuthentication + except ImportError: + raise ImportError("Install django-rest-knox to use REST_USE_KNOX = True") sensitive_post_parameters_m = method_decorator( sensitive_post_parameters( @@ -54,6 +61,8 @@ class LoginView(GenericAPIView): def get_response_serializer(self): if getattr(settings, 'REST_USE_JWT', False): response_serializer = JWTSerializer + elif getattr(settings, 'REST_USE_KNOX', False): + response_serializer = KnoxSerializer else: response_serializer = TokenSerializer return response_serializer @@ -63,6 +72,8 @@ class LoginView(GenericAPIView): if getattr(settings, 'REST_USE_JWT', False): self.token = jwt_encode(self.user) + elif getattr(settings, 'REST_USE_KNOX', False): + self.token = create_knox_token(self.user) else: self.token = create_token(self.token_model, self.user, self.serializer) @@ -80,6 +91,13 @@ class LoginView(GenericAPIView): } serializer = serializer_class(instance=data, context={'request': self.request}) + elif getattr(settings, 'REST_USE_KNOX', 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}) @@ -102,7 +120,11 @@ class LogoutView(APIView): Accepts/Returns nothing. """ - permission_classes = (AllowAny,) + if getattr(settings, 'REST_USE_KNOX', False): + authentication_classes = (KnoxTokenAuthentication,) + permission_classes = (IsAuthenticated,) + else: + permission_classes = (AllowAny,) def get(self, request, *args, **kwargs): if getattr(settings, 'ACCOUNT_LOGOUT_ON_GET', False): @@ -117,7 +139,44 @@ class LogoutView(APIView): def logout(self, request): try: - request.user.auth_token.delete() + if getattr(settings, 'REST_USE_KNOX', False): + request._auth.delete() + else: + request.user.auth_token.delete() + except (AttributeError, ObjectDoesNotExist): + pass + + django_logout(request) + + return Response({"detail": _("Successfully logged out.")}, + status=status.HTTP_200_OK) + + +class LogoutAllView(APIView): + """ + Calls Django logout method and deletes all the Knox tokens + assigned to the current User object. + + Accepts/Returns nothing. + """ + if getattr(settings, 'REST_USE_KNOX', False): + authentication_classes = (KnoxTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + def get(self, request, *args, **kwargs): + if getattr(settings, 'ACCOUNT_LOGOUT_ON_GET', False): + response = self.logout_all(request) + else: + response = self.http_method_not_allowed(request, *args, **kwargs) + + return self.finalize_response(request, response, *args, **kwargs) + + def post(self, request): + return self.logout_all(request) + + def logout_all(self, request): + try: + request.user.auth_token_set.all().delete() except (AttributeError, ObjectDoesNotExist): pass diff --git a/setup.py b/setup.py index 32ea6b7..6ff66d2 100644 --- a/setup.py +++ b/setup.py @@ -38,6 +38,8 @@ setup( tests_require=[ 'responses>=0.5.0', 'django-allauth>=0.25.0', + 'djangorestframework-jwt>=1.9.0', + 'django-rest-knox>=2.3.0' ], test_suite='runtests.runtests', include_package_data=True,