diff --git a/authentication/tests/test_logout_views.py b/authentication/tests/test_logout_views.py new file mode 100644 index 0000000000000000000000000000000000000000..bc5b3a66db213590387dcb9a54915314047ed66c --- /dev/null +++ b/authentication/tests/test_logout_views.py @@ -0,0 +1,48 @@ +import random + +import pytest +from django.urls import reverse +from rest_framework import status + +from authentication.models import AuthToken +from authentication.tests.factories import AuthTokenFactory + + +def assert_is_unauthorized(response): + assert response.status_code == status.HTTP_401_UNAUTHORIZED + + +@pytest.mark.parametrize( + "viewname,revoked_all", [("knox_logout", False), ("knox_logoutall", True)] +) +def test_logout(authenticated_api_client, account, viewname, revoked_all): + n_tokens = random.randrange(10, 21) + AuthTokenFactory.create_batch(n_tokens, user=account) + + # Also create some tokens that are not for this user + n_other_tokens = random.randrange(10, 21) + AuthTokenFactory.create_batch(n_other_tokens) + + # authenticated_api_client also created an authentication token for the user + assert account.auth_token_set.count() == n_tokens + 1 + + url = reverse(viewname) + + response = authenticated_api_client.post(url) + + assert response.status_code == status.HTTP_204_NO_CONTENT + + expected_count = 0 if revoked_all else n_tokens + assert account.auth_token_set.count() == expected_count + + # Tokens from other users should not be touched + assert AuthToken.objects.exclude(user=account).count() == n_other_tokens + + # We are unauthorised now, so trying to logout again will fail even while we provide an access + # token. + assert_is_unauthorized(authenticated_api_client.post(url)) + + +@pytest.mark.parametrize("viewname", ["knox_logout", "knox_logoutall"]) +def test_logout_unauthenticated(api_client, viewname): + assert_is_unauthorized(api_client.post(reverse(viewname))) diff --git a/authentication/urls.py b/authentication/urls.py index fb222f0ec5d39dd4a668a558f4fe01a3cd8a12d3..47b320c8b2d67c73d15c1c8ce9d40c1f85fe7cfa 100644 --- a/authentication/urls.py +++ b/authentication/urls.py @@ -1,14 +1,9 @@ from django.urls import path -from knox import views as knox_views -from .views import LoginView +from .views import LoginView, LogoutAllView, LogoutView urlpatterns = [ path("", LoginView.as_view(), name="knox_login"), - path("revoke/", knox_views.LogoutView.as_view(versioning_class=None), name="knox_logout"), - path( - "revoke/all/", - knox_views.LogoutAllView.as_view(versioning_class=None), - name="knox_logoutall", - ), + path("revoke/", LogoutView.as_view(), name="knox_logout"), + path("revoke/all/", LogoutAllView.as_view(), name="knox_logoutall"), ] diff --git a/authentication/views.py b/authentication/views.py index c1d40bf376e9091a9301f55a27780a5f55b82bc8..bc7c61a1c112f738541ee0cd6144f8a19d8de4a1 100644 --- a/authentication/views.py +++ b/authentication/views.py @@ -1,5 +1,5 @@ from knox.views import LoginView as KnoxLoginView -from rest_framework import parsers, renderers, serializers, status +from rest_framework import parsers, renderers, serializers, status, views from rest_framework.response import Response from authentication.errors import OAuth2Error @@ -68,3 +68,29 @@ class LoginView(KnoxLoginView): return default_handler(exc, context) return exception_handler + + +class LogoutView(views.APIView): + throttle_classes = () + versioning_class = None + renderer_classes = (renderers.JSONRenderer,) + + def get_post_response(self, request): + return Response(None, status=status.HTTP_204_NO_CONTENT) + + def post(self, request, format=None): + request._auth.delete() + return self.get_post_response(request) + + +class LogoutAllView(views.APIView): + throttle_classes = () + versioning_class = None + renderer_classes = (renderers.JSONRenderer,) + + def get_post_response(self, request): + return Response(None, status=status.HTTP_204_NO_CONTENT) + + def post(self, request, format=None): + request.user.auth_token_set.all().delete() + return self.get_post_response(request)