-
-
Notifications
You must be signed in to change notification settings - Fork 128
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #833 from kobotoolbox/digest-nonce-cache
django-digest redis cache nonce storage backend
- Loading branch information
Showing
10 changed files
with
127 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
from django.core.cache import caches | ||
from django_digest.utils import get_setting | ||
from redis.exceptions import LockError | ||
|
||
NONCE_NO_COUNT = '' # Needs to be something other than None to determine not set vs set to null | ||
|
||
|
||
class RedisCacheNonceStorage(): | ||
_blocking_timeout = 30 | ||
|
||
def _get_cache(self): | ||
# Dynamic fetching of cache is necessary to work with override_settings | ||
return caches[get_setting('DIGEST_NONCE_CACHE_NAME', 'default')] | ||
|
||
def _get_timeout(self): | ||
return get_setting('DIGEST_NONCE_TIMEOUT_IN_SECONDS', 5 * 60) | ||
|
||
def _generate_cache_key(self, user, nonce): | ||
return f'user_nonce_{user}_{nonce}' | ||
|
||
def update_existing_nonce(self, user, nonce, nonce_count): | ||
""" | ||
Check and update nonce record. If no record exists or has an invalid count, | ||
return False. Create a lock to prevent a concurrent replay attack where | ||
two requests are sent immediately and either may finish first. | ||
""" | ||
cache = self._get_cache() | ||
cache_key = self._generate_cache_key(user, nonce) | ||
|
||
if nonce_count == None: # No need to lock | ||
existing = cache.get(cache_key) | ||
if existing is None: | ||
return False | ||
cache.set(cache_key, NONCE_NO_COUNT, self._get_timeout()) | ||
else: | ||
try: | ||
with cache.lock( | ||
f'user_nonce_lock_{user}_{nonce}', | ||
timeout=self._get_timeout(), | ||
blocking_timeout=self._blocking_timeout | ||
): | ||
existing = cache.get(cache_key) | ||
if existing is None: | ||
return False | ||
if nonce_count <= existing: | ||
return False | ||
cache.set(cache_key, nonce_count, self._get_timeout()) | ||
except LockError: | ||
cache.delete(cache_key) | ||
return False | ||
return True | ||
|
||
def store_nonce(self, user, nonce, nonce_count): | ||
# Nonce is required | ||
if nonce is None or len(nonce) <= 1: | ||
return False | ||
if nonce_count is None: | ||
nonce_count = NONCE_NO_COUNT | ||
cache = self._get_cache() | ||
cache_key = self._generate_cache_key(user, nonce) | ||
return cache.set(cache_key, nonce_count, self._get_timeout()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
from django.core.cache import caches | ||
from django.test import TestCase | ||
|
||
from .cache import RedisCacheNonceStorage | ||
|
||
|
||
class TestCacheNonceStorage(TestCase): | ||
def setUp(self): | ||
self.test_user = 'bob' | ||
self.cache = caches['default'] | ||
self.storage = RedisCacheNonceStorage() | ||
|
||
def test_store_and_update(self): | ||
self.storage.store_nonce(self.test_user, 'testnonce', '') | ||
self.assertEqual(self.cache.get(f'user_nonce_{self.test_user}_testnonce'), '') | ||
|
||
# Should return true if the user + nonce already exists | ||
self.assertTrue(self.storage.update_existing_nonce(self.test_user, 'testnonce', None)) | ||
|
||
self.assertFalse(self.storage.update_existing_nonce(self.test_user, 'bogusnonce', None)) | ||
self.assertFalse(self.storage.update_existing_nonce('alice', 'testnonce', None)) | ||
|
||
self.cache.clear() | ||
self.assertFalse(self.storage.update_existing_nonce(self.test_user, 'testnonce', None)) | ||
# update should never create | ||
self.assertFalse(self.storage.update_existing_nonce(self.test_user, 'testnonce', None)) | ||
|
||
def test_update_count(self): | ||
self.storage.store_nonce(self.test_user, 'testnonce', 2) | ||
|
||
self.assertFalse(self.storage.update_existing_nonce(self.test_user, 'testnonce', 2)) | ||
self.assertTrue(self.storage.update_existing_nonce(self.test_user, 'testnonce', 3)) | ||
|
||
def test_nonce_lock(self): | ||
""" | ||
Lock timeout should be considered False and delete the nonce | ||
""" | ||
nonce = 'testnonce' | ||
self.storage._blocking_timeout = 0.1 | ||
self.storage.store_nonce(self.test_user, nonce, 1) | ||
with self.cache.lock(f'user_nonce_lock_{self.test_user}_{nonce}'): | ||
self.assertFalse(self.storage.update_existing_nonce(self.test_user, nonce, 2)) | ||
self.assertFalse(self.cache.get(self.storage._generate_cache_key(self.test_user, nonce))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters