Skip to content

Commit

Permalink
Add throttling to password reset
Browse files Browse the repository at this point in the history
  • Loading branch information
christophbuermann committed Nov 7, 2024
1 parent 553c555 commit cab097d
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 2 deletions.
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
2 changes: 2 additions & 0 deletions tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,5 @@
STATIC_URL = '/static/'

AUTH_USER_MODEL = "user_id_uuid_testapp.User"

REST_FRAMEWORK = {"DEFAULT_THROTTLE_RATES": {"django-rest-passwordreset-request-token": "2/day"}}
59 changes: 59 additions & 0 deletions tests/test/test_throttle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from django.urls import reverse
from rest_framework.test import APIClient, APITestCase
from django.contrib.auth.models import User
from django.contrib.auth import get_user_model
from django_rest_passwordreset.models import ResetPasswordToken
from throttling import ResetPasswordRequestTokenThrottle


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'))

def test_throttle(self,):
# first run on _reset_password_logged_in
# x number of runs (adjust number of calls to the throttle settings)
for i in range(2):
self._reset_password_logged_in()
# last run should raise an exception
self.assertRaises(Exception, self._reset_password_logged_in)

def test_default_rate(self):
class ThrottleWithAnotherScope(ResetPasswordRequestTokenThrottle):
scope = "throttle-with-another-scope"

self.assertEqual(ThrottleWithAnotherScope().rate, "3/day")

0 comments on commit cab097d

Please sign in to comment.