diff --git a/CHANGELOG.md b/CHANGELOG.md index 91e14522..d9e2ecec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.2.0 + +- Auto refresh now configurable per token in order to override global `AUTO_REFRESH` setting + ## 4.1.0 - Expiry format now defaults to whatever is used Django REST framework diff --git a/knox/auth.py b/knox/auth.py index c013c4dd..2b982c8f 100644 --- a/knox/auth.py +++ b/knox/auth.py @@ -72,7 +72,9 @@ def authenticate_credentials(self, token): except (TypeError, binascii.Error): raise exceptions.AuthenticationFailed(msg) if compare_digest(digest, auth_token.digest): - if knox_settings.AUTO_REFRESH and auth_token.expiry: + if auth_token.expiry and (knox_settings.AUTO_REFRESH and + auth_token.auto_refresh is None + or auth_token.auto_refresh): self.renew_token(auth_token) return self.validate_user(auth_token) raise exceptions.AuthenticationFailed(msg) diff --git a/knox/migrations/0008_authtoken_auto_refresh.py b/knox/migrations/0008_authtoken_auto_refresh.py new file mode 100644 index 00000000..8a23ed34 --- /dev/null +++ b/knox/migrations/0008_authtoken_auto_refresh.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.5 on 2019-09-15 13:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('knox', '0007_auto_20190111_0542'), + ] + + operations = [ + migrations.AddField( + model_name='authtoken', + name='auto_refresh', + field=models.NullBooleanField(), + ), + ] diff --git a/knox/models.py b/knox/models.py index 4dbee1ae..cc502046 100644 --- a/knox/models.py +++ b/knox/models.py @@ -9,7 +9,7 @@ class AuthTokenManager(models.Manager): - def create(self, user, expiry=knox_settings.TOKEN_TTL): + def create(self, user, expiry=knox_settings.TOKEN_TTL, auto_refresh=None): token = crypto.create_token_string() digest = crypto.hash_token(token) @@ -18,12 +18,11 @@ def create(self, user, expiry=knox_settings.TOKEN_TTL): instance = super(AuthTokenManager, self).create( token_key=token[:CONSTANTS.TOKEN_KEY_LENGTH], digest=digest, - user=user, expiry=expiry) + user=user, expiry=expiry, auto_refresh=auto_refresh) return instance, token class AuthToken(models.Model): - objects = AuthTokenManager() digest = models.CharField( @@ -34,6 +33,7 @@ class AuthToken(models.Model): related_name='auth_token_set', on_delete=models.CASCADE) created = models.DateTimeField(auto_now_add=True) expiry = models.DateTimeField(null=True, blank=True) + auto_refresh = models.NullBooleanField(null=True) def __str__(self): return '%s : %s' % (self.digest, self.user) diff --git a/setup.py b/setup.py index 1ab81ef4..cb221770 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version='4.1.0', + version='4.2.0', description='Authentication for django rest framework', long_description=long_description, long_description_content_type='text/markdown', diff --git a/tests/tests.py b/tests/tests.py index c50b5df1..4e1c62fc 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -316,6 +316,62 @@ def test_token_expiry_is_not_extended_within_MIN_REFRESH_INTERVAL(self): self.assertEqual(response.status_code, 200) self.assertEqual(original_expiry, AuthToken.objects.get().expiry) + def test_token_expiry_is_extended_for_token_with_auto_refresh_deativated(self): + self.assertEqual(knox_settings.AUTO_REFRESH, False) + self.assertEqual(knox_settings.TOKEN_TTL, timedelta(hours=10)) + + ttl = knox_settings.TOKEN_TTL + original_time = datetime(2018, 7, 25, 0, 0, 0, 0) + + with freeze_time(original_time): + instance, token = AuthToken.objects.create(user=self.user, auto_refresh=True) + + self.client.credentials(HTTP_AUTHORIZATION=('Token %s' % token)) + five_hours_later = original_time + timedelta(hours=5) + with freeze_time(five_hours_later): + response = self.client.get(root_url, {}, format='json') + reload_module(auth) + self.assertEqual(response.status_code, 200) + + # original expiry date was extended: + new_expiry = AuthToken.objects.get().expiry + expected_expiry = original_time + ttl + timedelta(hours=5) + self.assertEqual(new_expiry.replace(tzinfo=None), expected_expiry, + "Expiry time should have been extended to {} but is {}." + .format(expected_expiry, new_expiry)) + + # token works after original expiry: + after_original_expiry = original_time + ttl + timedelta(hours=1) + with freeze_time(after_original_expiry): + response = self.client.get(root_url, {}, format='json') + self.assertEqual(response.status_code, 200) + + # token does not work after new expiry: + new_expiry = AuthToken.objects.get().expiry + with freeze_time(new_expiry + timedelta(seconds=1)): + response = self.client.get(root_url, {}, format='json') + self.assertEqual(response.status_code, 401) + + def test_token_expiry_is_not_extended_for_token_with_auto_refresh_activated(self): + self.assertEqual(knox_settings.AUTO_REFRESH, False) + self.assertEqual(knox_settings.TOKEN_TTL, timedelta(hours=10)) + + now = datetime.now() + with freeze_time(now): + instance, token = AuthToken.objects.create(user=self.user, auto_refresh=False) + + original_expiry = AuthToken.objects.get().expiry + + self.client.credentials(HTTP_AUTHORIZATION=('Token %s' % token)) + with override_settings(REST_KNOX=auto_refresh_knox): + reload_module(auth) # necessary to reload settings in core code + with freeze_time(now + timedelta(hours=1)): + response = self.client.get(root_url, {}, format='json') + reload_module(auth) # necessary to reload settings in core code + + self.assertEqual(response.status_code, 200) + self.assertEqual(original_expiry, AuthToken.objects.get().expiry) + def test_expiry_signals(self): self.signal_was_called = False