Skip to content

Commit 275d1c4

Browse files
authored
Merge pull request #28 from mjlabe/logout-blacklist-jwt-token
Refresh token not blacklisted on logout
2 parents 058df2b + 1c64c0d commit 275d1c4

File tree

5 files changed

+90
-2
lines changed

5 files changed

+90
-2
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ target/
7272
# Jupyter Notebook
7373
.ipynb_checkpoints
7474

75+
# IDE
76+
.idea
77+
7578
# pyenv
7679
.python-version
7780

README.md

+5
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@ REST_USE_JWT = True
4444
JWT_AUTH_COOKIE = 'jwt-auth'
4545
```
4646

47+
### Testing
4748

49+
To run the tests within a virtualenv, run `python runtests.py` from the repository directory.
50+
The easiest way to run test coverage is with [`coverage`](https://pypi.org/project/coverage/),
51+
which runs the tests against all supported Django installs. To run the test coverage
52+
within a virtualenv, run `coverage run ./runtests.py` from the repository directory then run `coverage report`.
4853

4954

5055
### Documentation

dj_rest_auth/tests/settings.py

+2
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@
9494

9595
'dj_rest_auth',
9696
'dj_rest_auth.registration',
97+
98+
'rest_framework_simplejwt.token_blacklist'
9799
]
98100

99101
SECRET_KEY = "38dh*skf8sjfhs287dh&^hd8&3hdg*j2&sd"

dj_rest_auth/tests/test_api.py

+45-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import json
2+
13
from allauth.account import app_settings as account_app_settings
2-
from dj_rest_auth.registration.app_settings import register_permission_classes
3-
from dj_rest_auth.registration.views import RegisterView
44
from django.conf import settings
55
from django.contrib.auth import get_user_model
66
from django.core import mail
@@ -9,6 +9,8 @@
99
from rest_framework import status
1010
from rest_framework.test import APIRequestFactory
1111

12+
from dj_rest_auth.registration.app_settings import register_permission_classes
13+
from dj_rest_auth.registration.views import RegisterView
1214
from .mixins import CustomPermissionClass, TestsMixin
1315

1416
try:
@@ -555,3 +557,44 @@ def test_cookie_authentication(self):
555557
self.assertEqual(['jwt-auth'], list(resp.cookies.keys()))
556558
resp = self.get('/protected-view/')
557559
self.assertEquals(resp.status_code, 200)
560+
561+
@override_settings(REST_USE_JWT=True)
562+
def test_blacklisting_not_installed(self):
563+
settings.INSTALLED_APPS.remove('rest_framework_simplejwt.token_blacklist')
564+
payload = {
565+
"username": self.USERNAME,
566+
"password": self.PASS
567+
}
568+
get_user_model().objects.create_user(self.USERNAME, '', self.PASS)
569+
resp = self.post(self.login_url, data=payload, status_code=200)
570+
token = resp.data['refresh_token']
571+
resp = self.post(self.logout_url, status=200, data={'refresh': token})
572+
self.assertEqual(resp.status_code, 200)
573+
self.assertEqual(resp.data["detail"],
574+
"Neither cookies or blacklist are enabled, so the token has not been deleted server side. "
575+
"Please make sure the token is deleted client side.")
576+
577+
@override_settings(REST_USE_JWT=True)
578+
def test_blacklisting(self):
579+
payload = {
580+
"username": self.USERNAME,
581+
"password": self.PASS
582+
}
583+
get_user_model().objects.create_user(self.USERNAME, '', self.PASS)
584+
resp = self.post(self.login_url, data=payload, status_code=200)
585+
token = resp.data['refresh_token']
586+
# test refresh token not included in request data
587+
resp = self.post(self.logout_url, status=200)
588+
self.assertEqual(resp.status_code, 401)
589+
# test token is invalid or expired
590+
resp = self.post(self.logout_url, status=200, data={'refresh': '1'})
591+
self.assertEqual(resp.status_code, 401)
592+
# test successful logout
593+
resp = self.post(self.logout_url, status=200, data={'refresh': token})
594+
self.assertEqual(resp.status_code, 200)
595+
# test token is blacklisted
596+
resp = self.post(self.logout_url, status=200, data={'refresh': token})
597+
self.assertEqual(resp.status_code, 401)
598+
# test other TokenError, AttributeError, TypeError (invalid format)
599+
resp = self.post(self.logout_url, status=200, data=json.dumps({'refresh': token}))
600+
self.assertEqual(resp.status_code, 500)

dj_rest_auth/views.py

+35
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from rest_framework.permissions import AllowAny, IsAuthenticated
1212
from rest_framework.response import Response
1313
from rest_framework.views import APIView
14+
from rest_framework_simplejwt.exceptions import TokenError
15+
from rest_framework_simplejwt.tokens import RefreshToken
1416

1517
from .app_settings import (JWTSerializer, LoginSerializer,
1618
PasswordChangeSerializer,
@@ -132,15 +134,48 @@ def logout(self, request):
132134
request.user.auth_token.delete()
133135
except (AttributeError, ObjectDoesNotExist):
134136
pass
137+
135138
if getattr(settings, 'REST_SESSION_LOGIN', True):
136139
django_logout(request)
137140

138141
response = Response({"detail": _("Successfully logged out.")},
139142
status=status.HTTP_200_OK)
143+
140144
if getattr(settings, 'REST_USE_JWT', False):
141145
cookie_name = getattr(settings, 'JWT_AUTH_COOKIE', None)
142146
if cookie_name:
143147
response.delete_cookie(cookie_name)
148+
149+
elif 'rest_framework_simplejwt.token_blacklist' in settings.INSTALLED_APPS:
150+
# add refresh token to blacklist
151+
try:
152+
token = RefreshToken(request.data['refresh'])
153+
token.blacklist()
154+
155+
except KeyError:
156+
response = Response({"detail": _("Refresh token was not included in request data.")},
157+
status=status.HTTP_401_UNAUTHORIZED)
158+
159+
except (TokenError, AttributeError, TypeError) as error:
160+
if hasattr(error, 'args'):
161+
if 'Token is blacklisted' in error.args or 'Token is invalid or expired' in error.args:
162+
response = Response({"detail": _(error.args[0])},
163+
status=status.HTTP_401_UNAUTHORIZED)
164+
165+
else:
166+
response = Response({"detail": _("An error has occurred.")},
167+
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
168+
169+
else:
170+
response = Response({"detail": _("An error has occurred.")},
171+
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
172+
173+
else:
174+
response = Response({
175+
"detail": _("Neither cookies or blacklist are enabled, so the token has not been deleted server "
176+
"side. Please make sure the token is deleted client side."
177+
)}, status=status.HTTP_200_OK)
178+
144179
return response
145180

146181

0 commit comments

Comments
 (0)