Limit the rate of logins.
This commit is contained in:
@@ -3,7 +3,7 @@ django admin pages for courseware model
|
||||
'''
|
||||
|
||||
from external_auth.models import *
|
||||
from django.contrib import admin
|
||||
from ratelimitbackend import admin
|
||||
|
||||
|
||||
class ExternalAuthMapAdmin(admin.ModelAdmin):
|
||||
|
||||
@@ -9,12 +9,15 @@ from urlparse import parse_qs
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import TestCase, LiveServerTestCase
|
||||
from django.core.cache import cache
|
||||
from django.test.utils import override_settings
|
||||
# from django.contrib.auth.models import User
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test.client import RequestFactory
|
||||
from unittest import skipUnless
|
||||
|
||||
from student.tests.factories import UserFactory
|
||||
from external_auth.views import provider_login
|
||||
|
||||
|
||||
class MyFetcher(HTTPFetcher):
|
||||
"""A fetcher that uses server-internal calls for performing HTTP
|
||||
@@ -199,6 +202,49 @@ class OpenIdProviderTest(TestCase):
|
||||
""" Test for 403 error code when the url"""
|
||||
self.attempt_login(403, return_to="http://apps.cs50.edx.or")
|
||||
|
||||
def _send_bad_redirection_login(self):
|
||||
"""
|
||||
Attempt to log in to the provider with setup parameters
|
||||
|
||||
Intentionally fail the login to force a redirect
|
||||
"""
|
||||
user = UserFactory()
|
||||
|
||||
factory = RequestFactory()
|
||||
post_params = {'email': user.email, 'password': 'password'}
|
||||
fake_url = 'fake url'
|
||||
request = factory.post(reverse('openid-provider-login'), post_params)
|
||||
openid_setup = {
|
||||
'request': factory.request(),
|
||||
'url': fake_url
|
||||
}
|
||||
request.session = {
|
||||
'openid_setup': openid_setup
|
||||
}
|
||||
response = provider_login(request)
|
||||
return response
|
||||
|
||||
@skipUnless(settings.MITX_FEATURES.get('AUTH_USE_OPENID') or
|
||||
settings.MITX_FEATURES.get('AUTH_USE_OPENID_PROVIDER'), True)
|
||||
def test_login_openid_handle_redirection(self):
|
||||
""" Test to see that we can handle login redirection properly"""
|
||||
response = self._send_bad_redirection_login()
|
||||
self.assertEquals(response.status_code, 302)
|
||||
|
||||
@skipUnless(settings.MITX_FEATURES.get('AUTH_USE_OPENID') or
|
||||
settings.MITX_FEATURES.get('AUTH_USE_OPENID_PROVIDER'), True)
|
||||
def test_login_openid_handle_redirection_ratelimited(self):
|
||||
# try logging in 30 times, the default limit in the number of failed
|
||||
# log in attempts before the rate gets limited
|
||||
for _ in xrange(30):
|
||||
self._send_bad_redirection_login()
|
||||
|
||||
response = self._send_bad_redirection_login()
|
||||
# verify that we are not returning the default 403
|
||||
self.assertEquals(response.status_code, 302)
|
||||
# clear the ratelimit cache so that we don't fail other logins
|
||||
cache.clear()
|
||||
|
||||
|
||||
class OpenIdProviderLiveServerTest(LiveServerTestCase):
|
||||
"""
|
||||
|
||||
@@ -39,6 +39,7 @@ from openid.consumer.consumer import SUCCESS
|
||||
from openid.server.server import Server, ProtocolError, UntrustedReturnURL
|
||||
from openid.server.trustroot import TrustRoot
|
||||
from openid.extensions import ax, sreg
|
||||
from ratelimitbackend.exceptions import RateLimitException
|
||||
|
||||
import student.views as student_views
|
||||
# Required for Pearson
|
||||
@@ -191,7 +192,7 @@ def _external_login_or_signup(request,
|
||||
user.backend = auth_backend
|
||||
AUDIT_LOG.info('Linked user "%s" logged in via Shibboleth', user.email)
|
||||
else:
|
||||
user = authenticate(username=uname, password=eamap.internal_password)
|
||||
user = authenticate(username=uname, password=eamap.internal_password, request=request)
|
||||
if user is None:
|
||||
# we want to log the failure, but don't want to log the password attempted:
|
||||
AUDIT_LOG.warning('External Auth Login failed for "%s"', uname)
|
||||
@@ -718,7 +719,12 @@ def provider_login(request):
|
||||
# Failure is again redirected to the login dialog.
|
||||
username = user.username
|
||||
password = request.POST.get('password', None)
|
||||
user = authenticate(username=username, password=password)
|
||||
try:
|
||||
user = authenticate(username=username, password=password, request=request)
|
||||
except RateLimitException:
|
||||
AUDIT_LOG.warning('OpenID - Too many failed login attempts.')
|
||||
return HttpResponseRedirect(openid_request_url)
|
||||
|
||||
if user is None:
|
||||
request.session['openid_error'] = True
|
||||
msg = "OpenID login failed - password for %s is invalid"
|
||||
|
||||
@@ -4,7 +4,7 @@ django admin pages for courseware model
|
||||
|
||||
from student.models import UserProfile, UserTestGroup, CourseEnrollmentAllowed
|
||||
from student.models import CourseEnrollment, Registration, PendingNameChange
|
||||
from django.contrib import admin
|
||||
from ratelimitbackend import admin
|
||||
|
||||
admin.site.register(UserProfile)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from mock import patch
|
||||
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from django.core.cache import cache
|
||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
||||
from student.tests.factories import UserFactory, RegistrationFactory, UserProfileFactory
|
||||
|
||||
@@ -29,6 +30,7 @@ class LoginTest(TestCase):
|
||||
|
||||
# Create the test client
|
||||
self.client = Client()
|
||||
cache.clear()
|
||||
|
||||
# Store the login url
|
||||
try:
|
||||
@@ -95,6 +97,27 @@ class LoginTest(TestCase):
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self._assert_audit_log(mock_audit_log, 'info', [u'Logout', u'test'])
|
||||
|
||||
def test_login_ratelimited_success(self):
|
||||
# Try (and fail) logging in with fewer attempts than the limit of 30
|
||||
# and verify that you can still successfully log in afterwards.
|
||||
for i in xrange(20):
|
||||
password = u'test_password{0}'.format(i)
|
||||
response, _audit_log = self._login_response('test@edx.org', password)
|
||||
self._assert_response(response, success=False)
|
||||
# now try logging in with a valid password
|
||||
response, _audit_log = self._login_response('test@edx.org', 'test_password')
|
||||
self._assert_response(response, success=True)
|
||||
|
||||
def test_login_ratelimited(self):
|
||||
# try logging in 30 times, the default limit in the number of failed
|
||||
# login attempts in one 5 minute period before the rate gets limited
|
||||
for i in xrange(30):
|
||||
password = u'test_password{0}'.format(i)
|
||||
self._login_response('test@edx.org', password)
|
||||
# check to see if this response indicates that this was ratelimited
|
||||
response, _audit_log = self._login_response('test@edx.org', 'wrong_password')
|
||||
self._assert_response(response, success=False, value='Too many failed login attempts')
|
||||
|
||||
def _login_response(self, email, password, patched_audit_log='student.views.AUDIT_LOG'):
|
||||
''' Post the login info '''
|
||||
post_params = {'email': email, 'password': password}
|
||||
|
||||
@@ -28,6 +28,8 @@ from django.utils.http import cookie_date
|
||||
from django.utils.http import base36_to_int
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from ratelimitbackend.exceptions import RateLimitException
|
||||
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
@@ -421,13 +423,23 @@ def login_user(request, error=""):
|
||||
user = User.objects.get(email=email)
|
||||
except User.DoesNotExist:
|
||||
AUDIT_LOG.warning(u"Login failed - Unknown user email: {0}".format(email))
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'value': _('Email or password is incorrect.')})) # TODO: User error message
|
||||
user = None
|
||||
|
||||
username = user.username
|
||||
user = authenticate(username=username, password=password)
|
||||
# if the user doesn't exist, we want to set the username to an invalid
|
||||
# username so that authentication is guaranteed to fail and we can take
|
||||
# advantage of the ratelimited backend
|
||||
username = user.username if user else ""
|
||||
try:
|
||||
user = authenticate(username=username, password=password, request=request)
|
||||
# this occurs when there are too many attempts from the same IP address
|
||||
except RateLimitException:
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'value': _('Too many failed login attempts. Try again later.')}))
|
||||
if user is None:
|
||||
AUDIT_LOG.warning(u"Login failed - password for {0} is invalid".format(email))
|
||||
# if we didn't find this username earlier, the account for this email
|
||||
# doesn't exist, and doesn't have a corresponding password
|
||||
if username != "":
|
||||
AUDIT_LOG.warning(u"Login failed - password for {0} is invalid".format(email))
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'value': _('Email or password is incorrect.')}))
|
||||
|
||||
@@ -942,7 +954,7 @@ def auto_auth(request):
|
||||
# if they already are a user, log in
|
||||
try:
|
||||
user = User.objects.get(username=username)
|
||||
user = authenticate(username=username, password=password)
|
||||
user = authenticate(username=username, password=password, request=request)
|
||||
login(request, user)
|
||||
|
||||
# else create and activate account info
|
||||
|
||||
@@ -3,6 +3,6 @@ django admin pages for courseware model
|
||||
'''
|
||||
|
||||
from track.models import TrackingLog
|
||||
from django.contrib import admin
|
||||
from ratelimitbackend import admin
|
||||
|
||||
admin.site.register(TrackingLog)
|
||||
|
||||
Reference in New Issue
Block a user