From 74bc970edc0f5fba4588e7765211d8c4d4d55745 Mon Sep 17 00:00:00 2001 From: Waheed Ahmed <waheed.ahmed@arbisoft.com> Date: Wed, 20 May 2020 15:44:22 +0500 Subject: [PATCH] Rate limit logistration endpoints. PROD-1506 --- .../djangoapps/user_authn/views/login_form.py | 10 ++++++++ .../views/tests/test_logistration.py | 23 ++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/openedx/core/djangoapps/user_authn/views/login_form.py b/openedx/core/djangoapps/user_authn/views/login_form.py index 0d850eae304..1322d2eb441 100644 --- a/openedx/core/djangoapps/user_authn/views/login_form.py +++ b/openedx/core/djangoapps/user_authn/views/login_form.py @@ -7,6 +7,7 @@ import logging import six from django.conf import settings from django.contrib import messages +from django.http import HttpResponseForbidden from django.shortcuts import redirect from django.urls import reverse from django.utils.translation import ugettext as _ @@ -34,6 +35,7 @@ from student.helpers import get_next_url_for_login_page from third_party_auth import pipeline from third_party_auth.decorators import xframe_allow_whitelisted from util.password_policy_validators import DEFAULT_MAX_PASSWORD_LENGTH +from util.request_rate_limiter import BadRequestRateLimiter log = logging.getLogger(__name__) @@ -135,6 +137,12 @@ def login_and_registration_form(request, initial_mode="login"): initial_mode (string): Either "login" or "register". """ + + limiter = BadRequestRateLimiter() + if limiter.is_rate_limit_exceeded(request): + log.warning("Rate limit exceeded in login and registration with initial mode [%s]", initial_mode) + return HttpResponseForbidden("Rate limit exceeded") + # Determine the URL to redirect to following login/registration/third_party_auth redirect_to = get_next_url_for_login_page(request) @@ -230,6 +238,8 @@ def login_and_registration_form(request, initial_mode="login"): response = render_to_response('student_account/login_and_register.html', context) handle_enterprise_cookies_for_logistration(request, response, context) + limiter.tick_request_counter(request) + return response diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_logistration.py b/openedx/core/djangoapps/user_authn/views/tests/test_logistration.py index 1e0c4612e87..3c0d4881185 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_logistration.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_logistration.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ Tests for Logistration views. """ - +from datetime import datetime, timedelta from http.cookies import SimpleCookie import ddt @@ -17,6 +17,8 @@ from django.test.client import RequestFactory from django.test.utils import override_settings from django.urls import reverse from django.utils.translation import ugettext as _ +from freezegun import freeze_time +from pytz import UTC from six.moves.urllib.parse import urlencode # pylint: disable=import-error from course_modes.models import CourseMode @@ -71,6 +73,25 @@ class LoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMixin, ModuleSto expected_data = u'"initial_mode": "{mode}"'.format(mode=initial_mode) self.assertContains(response, expected_data) + def test_login_and_registration_form_ratelimited(self): + """ + Test that login enpoint allow only 30 requests for every 5 minutes. + """ + login_url = reverse('signin_user') + for i in range(30): + response = self.client.get(login_url) + self.assertEqual(response.status_code, 200) + + # then the rate limiter should kick in and give a HttpForbidden response + response = self.client.get(login_url) + self.assertEqual(response.status_code, 403) + + # now reset the time to 6 mins from now in future in order to unblock + reset_time = datetime.now(UTC) + timedelta(seconds=361) + with freeze_time(reset_time): + response = self.client.get(login_url) + self.assertEqual(response.status_code, 200) + @ddt.data("signin_user", "register_user") def test_login_and_registration_form_already_authenticated(self, url_name): # call the account registration api that sets the login cookies -- GitLab