Implement both IP and email based rate limiting.

This commit is contained in:
Waheed Ahmed
2020-04-30 20:23:12 +05:00
parent f3db71171e
commit 05d18effde
3 changed files with 87 additions and 9 deletions

View File

@@ -247,8 +247,11 @@ def password_reset(request):
if password_reset_email_limiter.is_rate_limit_exceeded(request):
AUDIT_LOG.warning("Password reset rate limit exceeded")
return HttpResponse(
_("Your previous request is in progress, please try again in a few moments."),
return JsonResponse(
{
'success': False,
'value': _("Your previous request is in progress, please try again in a few moments.")
},
status=403
)

View File

@@ -123,16 +123,33 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase):
'openedx.core.djangoapps.user_authn.views.password_reset.render_to_string',
Mock(side_effect=mock_render_to_string, autospec=True)
)
@ddt.data(True, False)
def test_password_reset_ratelimited(self, existing_user):
def test_password_reset_ratelimited_for_non_existing_user(self):
"""
Test that reset password endpoint only allow one request per minute for both
existing and non-existing users.
Test that reset password endpoint only allow one request per minute
for non-existing user.
"""
self.assert_password_reset_ratelimitted('thisdoesnotexist@foo.com', AnonymousUser())
self.assert_no_events_were_emitted()
@patch(
'openedx.core.djangoapps.user_authn.views.password_reset.render_to_string',
Mock(side_effect=mock_render_to_string, autospec=True)
)
def test_password_reset_ratelimited_for_existing_user(self):
"""
Test that reset password endpoint only allow one request per minute
for existing user.
"""
self.assert_password_reset_ratelimitted(self.user.email, self.user)
self.assert_event_emission_count(SETTING_CHANGE_INITIATED, 1)
def assert_password_reset_ratelimitted(self, email, user):
"""
Assert that password reset endpoint allow one request per minute per email.
"""
cache.clear()
email = self.user.email if existing_user else 'thisdoesnotexist@foo.com'
password_reset_req = self.request_factory.post('/password_reset/', {'email': email})
password_reset_req.user = self.user if existing_user else AnonymousUser()
password_reset_req.user = user
password_reset_req.site = Mock(domain='example.com')
good_resp = password_reset(password_reset_req)
self.assertEqual(good_resp.status_code, 200)
@@ -143,6 +160,29 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase):
cache.clear()
@patch(
'openedx.core.djangoapps.user_authn.views.password_reset.render_to_string',
Mock(side_effect=mock_render_to_string, autospec=True)
)
def test_ratelimitted_from_same_ip_with_different_email(self):
"""
Test that password reset endpoint allow one request per minute per IP.
"""
cache.clear()
good_req = self.request_factory.post('/password_reset/', {'email': 'thisdoesnotexist@foo.com'})
good_req.user = AnonymousUser()
good_resp = password_reset(good_req)
self.assertEqual(good_resp.status_code, 200)
# change the email ID and verify that the rate limiter should kick in and
# give a Forbidden response if the request is from same IP.
bad_req = self.request_factory.post('/password_reset/', {'email': 'thisdoesnotexist2@foo.com'})
bad_req.user = AnonymousUser()
bad_resp = password_reset(bad_req)
self.assertEqual(bad_resp.status_code, 403)
cache.clear()
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS")
@ddt.data(('plain_text', "You're receiving this e-mail because you requested a password reset"),
('html', "You're receiving this e-mail because you requested a password reset"))