Skip to content

Commit

Permalink
Merge pull request #200 from anexia-it/SIANXKE-366
Browse files Browse the repository at this point in the history
Add throttling to password reset
  • Loading branch information
nezhar authored Nov 13, 2024
2 parents 2eced20 + 48ef5b7 commit 09517ba
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 12 deletions.
13 changes: 3 additions & 10 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,11 @@ jobs:
python setup.py install
- name: Run tests
working-directory: ./tests
run: |
# prepare Django project: link all necessary data from the test project into the root directory
# Hint: Simply changing the directory does not work (leads to missing files in coverage report)
ln -s ./tests/test test
ln -s ./tests/user_id_uuid_testapp user_id_uuid_testapp
ln -s ./tests/manage.py manage.py
ln -s ./tests/settings.py settings.py
ln -s ./tests/urls.py urls.py
# run tests with coverage
ln -s ../django_rest_passwordreset django_rest_passwordreset
coverage run --source='./django_rest_passwordreset' manage.py test
coverage xml
coverage xml -o ../coverage.xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ build/
.tox/
db.sqlite3
dist/

.DS_Store
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,20 @@ class RandomStringTokenGenerator(BaseTokenGenerator):
```


### Throttling

The endpoint to request a reset password token provides throttling.
Per default the throttling rate is `3/day` per IP address.

The throttling rate can be customized using the `REST_FRAMEWORK` setting and the scope `"django-rest-passwordreset-request-token"`:

```
REST_FRAMEWORK = {"DEFAULT_THROTTLE_RATES": {"django-rest-passwordreset-request-token": "5/hour"}}
```

See also: https://www.django-rest-framework.org/api-guide/throttling/#setting-the-throttling-policy


## Compatibility Matrix

This library should be compatible with the latest Django and Django Rest Framework Versions. For reference, here is
Expand Down
16 changes: 16 additions & 0 deletions django_rest_passwordreset/throttling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.core.exceptions import ImproperlyConfigured
from rest_framework.throttling import UserRateThrottle


__all__ = ("ResetPasswordRequestTokenThrottle",)


class ResetPasswordRequestTokenThrottle(UserRateThrottle):
scope = "django-rest-passwordreset-request-token"
default_rate = "3/day"

def get_rate(self):
try:
return super().get_rate()
except ImproperlyConfigured:
return self.default_rate
3 changes: 2 additions & 1 deletion django_rest_passwordreset/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
get_password_reset_lookup_field
from django_rest_passwordreset.serializers import EmailSerializer, PasswordTokenSerializer, ResetTokenSerializer
from django_rest_passwordreset.signals import reset_password_token_created, pre_password_reset, post_password_reset
from django_rest_passwordreset.throttling import ResetPasswordRequestTokenThrottle

User = get_user_model()

Expand Down Expand Up @@ -185,7 +186,7 @@ class ResetPasswordRequestToken(GenericAPIView):
Sends a signal reset_password_token_created when a reset token was created
"""
throttle_classes = ()
throttle_classes = (ResetPasswordRequestTokenThrottle,)
permission_classes = ()
serializer_class = EmailSerializer
authentication_classes = ()
Expand Down
7 changes: 7 additions & 0 deletions tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,10 @@
STATIC_URL = '/static/'

AUTH_USER_MODEL = "user_id_uuid_testapp.User"

REST_FRAMEWORK = {
"DEFAULT_THROTTLE_RATES": {
"django-rest-passwordreset-request-token": "99999/second",
"django-rest-passwordreset-request-token-test-scope": "1/day",
},
}
70 changes: 70 additions & 0 deletions tests/test/test_throttle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from unittest import mock

from rest_framework.test import APIClient, APITestCase
from django.contrib.auth import get_user_model
from django.urls import reverse

from django_rest_passwordreset.models import ResetPasswordToken


User = get_user_model()


class TestThrottle(APITestCase):
def setUp(self):
self.client = APIClient()

self.user_1 = User.objects.create_user(username='user_1', password='password', email='[email protected]')
self.user_admin = User.objects.create_superuser(username='admin', password='password', email='[email protected]')

def _reset_password_logged_in(self):

# generate token
url = reverse('password_reset:reset-password-request')
data = {'email': self.user_1.email}
response = self.client.post(url, data).json()

self.assertIn('status', response)
self.assertEqual(response['status'], 'OK')

# test validity of the token
token = ResetPasswordToken.objects.filter(user=self.user_1).first()
url = reverse('password_reset:reset-password-validate')
data = {'token': token.key}
response = self.client.post(url, data).json()

self.assertIn('status', response)
self.assertEqual(response['status'], 'OK')

# reset password
url = reverse('password_reset:reset-password-confirm')
data = {'token': token.key, 'password': 'new_password'}
response = self.client.post(url, data).json()

# check if new password was set
self.assertTrue(token.user.check_password('new_password'))
self.assertFalse(token.user.check_password('password'))

@mock.patch(
"django_rest_passwordreset.throttling.ResetPasswordRequestTokenThrottle.scope",
"django-rest-passwordreset-request-token-test-scope",
)
def test_throttle(self,):
# first run on _reset_password_logged_in
# x number of runs (adjust number of calls to the throttle rate)
for _ in range(1):
self._reset_password_logged_in()
# last run should raise an exception
self.assertRaises(Exception, self._reset_password_logged_in)

@mock.patch(
"django_rest_passwordreset.throttling.ResetPasswordRequestTokenThrottle.scope",
"django-rest-passwordreset-request-token-does-not-exist",
)
def test_throttle_default_rate(self):
# first run on _reset_password_logged_in
# x number of runs (adjust number of calls to the throttle rate)
for _ in range(3):
self._reset_password_logged_in()
# last run should raise an exception
self.assertRaises(Exception, self._reset_password_logged_in)

0 comments on commit 09517ba

Please sign in to comment.