diff --git a/.gitignore b/.gitignore index 84cc2de..136132c 100644 --- a/.gitignore +++ b/.gitignore @@ -72,6 +72,9 @@ target/ # Jupyter Notebook .ipynb_checkpoints +# IDE +.idea + # pyenv .python-version diff --git a/README.md b/README.md index b87ba30..deefdb5 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,12 @@ REST_USE_JWT = True JWT_AUTH_COOKIE = 'jwt-auth' ``` +### Testing +To run the tests within a virtualenv, run `python runtests.py` from the repository directory. +The easiest way to run test coverage is with [`coverage`](https://pypi.org/project/coverage/), +which runs the tests against all supported Django installs. To run the test coverage +within a virtualenv, run `coverage run ./runtests.py` from the repository directory then run `coverage report`. ### Documentation diff --git a/dj_rest_auth/tests/settings.py b/dj_rest_auth/tests/settings.py index 3d02309..5f57f91 100644 --- a/dj_rest_auth/tests/settings.py +++ b/dj_rest_auth/tests/settings.py @@ -94,6 +94,8 @@ INSTALLED_APPS = [ 'dj_rest_auth', 'dj_rest_auth.registration', + + 'rest_framework_simplejwt.token_blacklist' ] SECRET_KEY = "38dh*skf8sjfhs287dh&^hd8&3hdg*j2&sd" diff --git a/dj_rest_auth/tests/test_api.py b/dj_rest_auth/tests/test_api.py index 0134560..c53be66 100644 --- a/dj_rest_auth/tests/test_api.py +++ b/dj_rest_auth/tests/test_api.py @@ -1,6 +1,6 @@ +import json + from allauth.account import app_settings as account_app_settings -from dj_rest_auth.registration.app_settings import register_permission_classes -from dj_rest_auth.registration.views import RegisterView from django.conf import settings from django.contrib.auth import get_user_model from django.core import mail @@ -9,6 +9,8 @@ from django.utils.encoding import force_text from rest_framework import status from rest_framework.test import APIRequestFactory +from dj_rest_auth.registration.app_settings import register_permission_classes +from dj_rest_auth.registration.views import RegisterView from .mixins import CustomPermissionClass, TestsMixin try: @@ -555,3 +557,44 @@ class APIBasicTests(TestsMixin, TestCase): self.assertEqual(['jwt-auth'], list(resp.cookies.keys())) resp = self.get('/protected-view/') self.assertEquals(resp.status_code, 200) + + @override_settings(REST_USE_JWT=True) + def test_blacklisting_not_installed(self): + settings.INSTALLED_APPS.remove('rest_framework_simplejwt.token_blacklist') + payload = { + "username": self.USERNAME, + "password": self.PASS + } + get_user_model().objects.create_user(self.USERNAME, '', self.PASS) + resp = self.post(self.login_url, data=payload, status_code=200) + token = resp.data['refresh_token'] + resp = self.post(self.logout_url, status=200, data={'refresh': token}) + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.data["detail"], + "Neither cookies or blacklist are enabled, so the token has not been deleted server side. " + "Please make sure the token is deleted client side.") + + @override_settings(REST_USE_JWT=True) + def test_blacklisting(self): + payload = { + "username": self.USERNAME, + "password": self.PASS + } + get_user_model().objects.create_user(self.USERNAME, '', self.PASS) + resp = self.post(self.login_url, data=payload, status_code=200) + token = resp.data['refresh_token'] + # test refresh token not included in request data + resp = self.post(self.logout_url, status=200) + self.assertEqual(resp.status_code, 401) + # test token is invalid or expired + resp = self.post(self.logout_url, status=200, data={'refresh': '1'}) + self.assertEqual(resp.status_code, 401) + # test successful logout + resp = self.post(self.logout_url, status=200, data={'refresh': token}) + self.assertEqual(resp.status_code, 200) + # test token is blacklisted + resp = self.post(self.logout_url, status=200, data={'refresh': token}) + self.assertEqual(resp.status_code, 401) + # test other TokenError, AttributeError, TypeError (invalid format) + resp = self.post(self.logout_url, status=200, data=json.dumps({'refresh': token})) + self.assertEqual(resp.status_code, 500) diff --git a/dj_rest_auth/views.py b/dj_rest_auth/views.py index c5ba7fc..114856b 100644 --- a/dj_rest_auth/views.py +++ b/dj_rest_auth/views.py @@ -11,6 +11,8 @@ from rest_framework.generics import GenericAPIView, RetrieveUpdateAPIView from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView +from rest_framework_simplejwt.exceptions import TokenError +from rest_framework_simplejwt.tokens import RefreshToken from .app_settings import (JWTSerializer, LoginSerializer, PasswordChangeSerializer, @@ -132,15 +134,48 @@ class LogoutView(APIView): request.user.auth_token.delete() except (AttributeError, ObjectDoesNotExist): pass + if getattr(settings, 'REST_SESSION_LOGIN', True): django_logout(request) response = Response({"detail": _("Successfully logged out.")}, status=status.HTTP_200_OK) + if getattr(settings, 'REST_USE_JWT', False): cookie_name = getattr(settings, 'JWT_AUTH_COOKIE', None) if cookie_name: response.delete_cookie(cookie_name) + + elif 'rest_framework_simplejwt.token_blacklist' in settings.INSTALLED_APPS: + # add refresh token to blacklist + try: + token = RefreshToken(request.data['refresh']) + token.blacklist() + + except KeyError: + response = Response({"detail": _("Refresh token was not included in request data.")}, + status=status.HTTP_401_UNAUTHORIZED) + + except (TokenError, AttributeError, TypeError) as error: + if hasattr(error, 'args'): + if 'Token is blacklisted' in error.args or 'Token is invalid or expired' in error.args: + response = Response({"detail": _(error.args[0])}, + status=status.HTTP_401_UNAUTHORIZED) + + else: + response = Response({"detail": _("An error has occurred.")}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + else: + response = Response({"detail": _("An error has occurred.")}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + else: + response = Response({ + "detail": _("Neither cookies or blacklist are enabled, so the token has not been deleted server " + "side. Please make sure the token is deleted client side." + )}, status=status.HTTP_200_OK) + return response