Skip to content

Commit a3dfcba

Browse files
Merge pull request #366 from christian-oudard/develop
Implement AUTO_REFRESH_MAX_TTL to limit total token lifetime when AUTO_REFRESH = True.
2 parents 7d564c1 + d13ff2a commit a3dfcba

File tree

4 files changed

+52
-0
lines changed

4 files changed

+52
-0
lines changed

docs/settings.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ REST_KNOX = {
2020
'USER_SERIALIZER': 'knox.serializers.UserSerializer',
2121
'TOKEN_LIMIT_PER_USER': None,
2222
'AUTO_REFRESH': False,
23+
'AUTO_REFRESH_MAX_TTL': None,
2324
'MIN_REFRESH_INTERVAL': 60,
2425
'AUTH_HEADER_PREFIX': 'Token',
2526
'EXPIRY_DATETIME_FORMAT': api_settings.DATETIME_FORMAT,
@@ -78,6 +79,11 @@ successfully returning from `LoginView`. The default is `knox.serializers.UserSe
7879
This defines if the token expiry time is extended by TOKEN_TTL each time the token
7980
is used.
8081

82+
## AUTO_REFRESH_MAX_TTL
83+
When automatically extending token expiry time, limit the total token lifetime. If
84+
AUTO_REFRESH_MAX_TTL is set, then the token lifetime since the original creation date cannot
85+
exceed AUTO_REFRESH_MAX_TTL.
86+
8187
## MIN_REFRESH_INTERVAL
8288
This is the minimum time in seconds that needs to pass for the token expiry to be updated
8389
in the database.

knox/auth.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import binascii
2+
import logging
23
from hmac import compare_digest
34

45
from django.utils import timezone
@@ -13,6 +14,8 @@
1314
from knox.settings import CONSTANTS, knox_settings
1415
from knox.signals import token_expired
1516

17+
logger = logging.getLogger(__name__)
18+
1619

1720
class TokenAuthentication(BaseAuthentication):
1821
'''
@@ -74,7 +77,16 @@ def authenticate_credentials(self, token):
7477
def renew_token(self, auth_token) -> None:
7578
current_expiry = auth_token.expiry
7679
new_expiry = timezone.now() + knox_settings.TOKEN_TTL
80+
81+
# Do not auto-renew tokens past AUTO_REFRESH_MAX_TTL.
82+
if knox_settings.AUTO_REFRESH_MAX_TTL is not None:
83+
max_expiry = auth_token.created + knox_settings.AUTO_REFRESH_MAX_TTL
84+
if new_expiry > max_expiry:
85+
new_expiry = max_expiry
86+
logger.info('Token renewal truncated due to AUTO_REFRESH_MAX_TTL.')
87+
7788
auth_token.expiry = new_expiry
89+
7890
# Throttle refreshing of token to avoid db writes
7991
delta = (new_expiry - current_expiry).total_seconds()
8092
if delta > knox_settings.MIN_REFRESH_INTERVAL:

knox/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
'USER_SERIALIZER': None,
1414
'TOKEN_LIMIT_PER_USER': None,
1515
'AUTO_REFRESH': False,
16+
'AUTO_REFRESH_MAX_TTL': None,
1617
'MIN_REFRESH_INTERVAL': 60,
1718
'AUTH_HEADER_PREFIX': 'Token',
1819
'EXPIRY_DATETIME_FORMAT': api_settings.DATETIME_FORMAT,

tests/tests.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ def get_basic_auth_header(username, password):
2929
auto_refresh_knox = knox_settings.defaults.copy()
3030
auto_refresh_knox["AUTO_REFRESH"] = True
3131

32+
auto_refresh_max_ttl_knox = auto_refresh_knox.copy()
33+
auto_refresh_max_ttl_knox["AUTO_REFRESH_MAX_TTL"] = timedelta(hours=12)
34+
3235
token_user_limit_knox = knox_settings.defaults.copy()
3336
token_user_limit_knox["TOKEN_LIMIT_PER_USER"] = 10
3437

@@ -318,6 +321,36 @@ def test_token_expiry_is_not_extended_within_MIN_REFRESH_INTERVAL(self):
318321
self.assertEqual(response.status_code, 200)
319322
self.assertEqual(original_expiry, AuthToken.objects.get().expiry)
320323

324+
def test_token_expiry_is_not_extended_past_max_ttl(self):
325+
ttl = knox_settings.TOKEN_TTL
326+
self.assertEqual(ttl, timedelta(hours=10))
327+
original_time = datetime(2018, 7, 25, 0, 0, 0, 0)
328+
329+
with freeze_time(original_time):
330+
instance, token = AuthToken.objects.create(user=self.user)
331+
332+
self.client.credentials(HTTP_AUTHORIZATION=('Token %s' % token))
333+
five_hours_later = original_time + timedelta(hours=5)
334+
with override_settings(REST_KNOX=auto_refresh_max_ttl_knox):
335+
reload(auth) # necessary to reload settings in core code
336+
self.assertEqual(auth.knox_settings.AUTO_REFRESH, True)
337+
self.assertEqual(auth.knox_settings.AUTO_REFRESH_MAX_TTL, timedelta(hours=12))
338+
with freeze_time(five_hours_later):
339+
response = self.client.get(root_url, {}, format='json')
340+
reload(auth) # necessary to reload settings in core code
341+
self.assertEqual(response.status_code, 200)
342+
343+
# original expiry date was extended, but not past max_ttl:
344+
new_expiry = AuthToken.objects.get().expiry
345+
expected_expiry = original_time + timedelta(hours=12)
346+
self.assertEqual(new_expiry.replace(tzinfo=None), expected_expiry,
347+
"Expiry time should have been extended to {} but is {}."
348+
.format(expected_expiry, new_expiry))
349+
350+
with freeze_time(expected_expiry + timedelta(seconds=1)):
351+
response = self.client.get(root_url, {}, format='json')
352+
self.assertEqual(response.status_code, 401)
353+
321354
def test_expiry_signals(self):
322355
self.signal_was_called = False
323356

0 commit comments

Comments
 (0)