Skip to content

Commit 6e8b6dd

Browse files
committed
knox: handle race condition on concurrent logout calls
With AUTO_REFRESH enabled in case of multiple logout calls when one has removed the token while the other is still authenticating we may get a DatabaseError on TokenAuthentication.renew_token while updating the expiry of a now removed auth_token. Fix that by ignoring the token that returned a database error so that in case of double logout the last request will get an AuthenticationFailed exception instead of a 500 error.
1 parent 78e72ba commit 6e8b6dd

File tree

4 files changed

+30
-1
lines changed

4 files changed

+30
-1
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## UNRELEASED
2+
3+
- Fix race condition on concurrent logout calls
4+
15
## 4.1.0
26

37
- Expiry format now defaults to whatever is used Django REST framework

knox/auth.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ def compare_digest(a, b):
77
import binascii
88

99
from django.contrib.auth import get_user_model
10+
from django.db import DatabaseError
1011
from django.utils import timezone
1112
from django.utils.translation import ugettext_lazy as _
1213
from rest_framework import exceptions
@@ -73,7 +74,11 @@ def authenticate_credentials(self, token):
7374
raise exceptions.AuthenticationFailed(msg)
7475
if compare_digest(digest, auth_token.digest):
7576
if knox_settings.AUTO_REFRESH and auth_token.expiry:
76-
self.renew_token(auth_token)
77+
try:
78+
self.renew_token(auth_token)
79+
except DatabaseError:
80+
# avoid race condition with concurrent logout calls
81+
continue
7782
return self.validate_user(auth_token)
7883
raise exceptions.AuthenticationFailed(msg)
7984

tests/tests.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
from datetime import datetime, timedelta
33

44
from django.contrib.auth import get_user_model
5+
from django.db import DatabaseError
56
from django.test import override_settings
67
from django.utils.six.moves import reload_module
78
from freezegun import freeze_time
9+
from rest_framework.exceptions import AuthenticationFailed
810
from rest_framework.serializers import DateTimeField
911
from rest_framework.test import APIRequestFactory, APITestCase as TestCase
1012

@@ -15,6 +17,13 @@
1517
from knox.settings import CONSTANTS, knox_settings
1618
from knox.signals import token_expired
1719

20+
try:
21+
# Python 3
22+
from unittest import mock
23+
except ImportError:
24+
# Python 2
25+
import mock
26+
1827
try:
1928
# For django >= 2.0
2029
from django.urls import reverse
@@ -396,3 +405,13 @@ def test_expiry_is_present(self):
396405
response.data['expiry'],
397406
DateTimeField().to_representation(AuthToken.objects.first().expiry)
398407
)
408+
409+
def test_authenticate_credentials_handle_database_error_on_renew_token(self):
410+
instance, token = AuthToken.objects.create(user=self.user)
411+
with override_settings(REST_KNOX=auto_refresh_knox):
412+
reload_module(auth)
413+
token_auth = TokenAuthentication()
414+
with mock.patch.object(token_auth, 'renew_token') as m:
415+
m.side_effect = DatabaseError()
416+
with self.assertRaises(AuthenticationFailed):
417+
token_auth.authenticate_credentials(token.encode('utf-8'))

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ deps =
3535
django22: Django>=2.2,<2.3
3636
django-nose
3737
markdown<3.0
38+
mock
3839
isort
3940
djangorestframework
4041
freezegun

0 commit comments

Comments
 (0)