diff --git a/lms/djangoapps/oauth2_handler/tests.py b/lms/djangoapps/oauth2_handler/tests.py index e9316170db..4994620338 100644 --- a/lms/djangoapps/oauth2_handler/tests.py +++ b/lms/djangoapps/oauth2_handler/tests.py @@ -1,4 +1,5 @@ # pylint: disable=missing-docstring +import mock from django.core.cache import cache from django.test.utils import override_settings # Will also run default tests for IDTokens and UserInfo @@ -134,6 +135,13 @@ class IDTokenTest(BaseTestMixin, IDTokenTestCase): _scopes, claims = self.get_id_token_values('openid profile permissions') self.assertTrue(claims['administrator']) + def test_rate_limit_token(self): + with mock.patch('openedx.core.djangoapps.oauth_dispatch.views.AccessTokenView.ratelimit_rate', '1/m'): + response = self.get_access_token_response('openid profile permissions') + self.assertEqual(response.status_code, 200) + response = self.get_access_token_response('openid profile permissions') + self.assertEqual(response.status_code, 403) + class UserInfoTest(BaseTestMixin, UserInfoTestCase): def setUp(self): diff --git a/lms/envs/common.py b/lms/envs/common.py index 4b218ad47e..3220992aa6 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3465,3 +3465,7 @@ EDX_PLATFORM_REVISION = 'unknown' # Once a user has watched this percentage of a video, mark it as complete: # (0.0 = 0%, 1.0 = 100%) COMPLETION_VIDEO_COMPLETE_PERCENTAGE = 0.95 + +############### Settings for Django Rate limit ##################### +RATELIMIT_ENABLE = True +RATELIMIT_RATE = '30/m' diff --git a/openedx/core/djangoapps/oauth_dispatch/views.py b/openedx/core/djangoapps/oauth_dispatch/views.py index 335ea361d8..8eae354de1 100644 --- a/openedx/core/djangoapps/oauth_dispatch/views.py +++ b/openedx/core/djangoapps/oauth_dispatch/views.py @@ -17,6 +17,8 @@ from edx_oauth2_provider import views as dop_views # django-oauth2-provider vie from jwkest.jwk import RSAKey from oauth2_provider import models as dot_models # django-oauth-toolkit from oauth2_provider import views as dot_views +from ratelimit import ALL +from ratelimit.mixins import RatelimitMixin from openedx.core.djangoapps.auth_exchange import views as auth_exchange_views from openedx.core.lib.token_utils import JwtBuilder @@ -83,12 +85,16 @@ class _DispatchingView(View): return request.POST.get('client_id') -class AccessTokenView(_DispatchingView): +class AccessTokenView(RatelimitMixin, _DispatchingView): """ Handle access token requests. """ dot_view = dot_views.TokenView dop_view = dop_views.AccessTokenView + ratelimit_key = 'openedx.core.djangoapps.util.ratelimit.real_ip' + ratelimit_rate = settings.RATELIMIT_RATE + ratelimit_block = True + ratelimit_method = ALL def dispatch(self, request, *args, **kwargs): response = super(AccessTokenView, self).dispatch(request, *args, **kwargs) diff --git a/openedx/core/djangoapps/util/ratelimit.py b/openedx/core/djangoapps/util/ratelimit.py new file mode 100644 index 0000000000..e82183609b --- /dev/null +++ b/openedx/core/djangoapps/util/ratelimit.py @@ -0,0 +1,5 @@ +from ipware.ip import get_ip + + +def real_ip(group, request): + return get_ip(request) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 79cc629b60..c6669e944a 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -29,6 +29,7 @@ django-mptt>=0.8.6,<0.9 django-oauth-toolkit==0.12.0 django-pipeline-forgiving==1.0.0 django-pyfs==2.0 +django-ratelimit==1.1.0 django-sekizai>=0.10 django-ses==0.8.4 django-simple-history==1.9.0