Skip to content

Commit b61e9dc

Browse files
committed
knox: handle race condition on concurrent logout call
With AUTO_REFRESH enabled authentication is racing against token removal of logout. If a token gets removed updating its expiry would led to a DatabaseError raised by the Django ORM. Fix that by ignoring DatabaseError exception returned by renew_token so that the last request will get a AuthenticationFailed exception instead of a 500 error.
1 parent 19df006 commit b61e9dc

File tree

4 files changed

+36
-1
lines changed

4 files changed

+36
-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 call
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+
break
7782
return self.validate_user(auth_token)
7883
raise exceptions.AuthenticationFailed(msg)
7984

tests/tests.py

Lines changed: 25 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,19 @@ 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_expiry_update_of_gone_token(self):
410+
"""This tests a race condition of an authentication against logout
411+
412+
It may happen that a token gets delete while we are inside
413+
authenticate_credentials and the sympton would be django ORM raising
414+
a DatabaseError when updating the expiry time."""
415+
416+
instance, token = AuthToken.objects.create(user=self.user)
417+
with override_settings(REST_KNOX=auto_refresh_knox):
418+
reload_module(auth)
419+
token_auth = TokenAuthentication()
420+
with mock.patch.object(token_auth, 'renew_token') as m:
421+
m.side_effect = DatabaseError()
422+
with self.assertRaises(AuthenticationFailed):
423+
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)