Commit 53e96524 authored by Sofia Papagiannaki's avatar Sofia Papagiannaki

Ratelimit login attempts

Refs: #2267
parent c56b7621
......@@ -40,9 +40,9 @@ Settings
Configure in ``settings.py`` or a ``.conf`` file in ``/etc/synnefo`` if using snf-webproject.
============================== ============================================================================= ===========================================================================================
================================= ============================================================================= ===========================================================================================
Name Default value Description
============================== ============================================================================= ===========================================================================================
================================= ============================================================================= ===========================================================================================
ASTAKOS_AUTH_TOKEN_DURATION one month Expiration time of newly created auth tokens
ASTAKOS_DEFAULT_USER_LEVEL 4 Default (not-invited) user level
ASTAKOS_INVITATIONS_PER_LEVEL {0:100, 1:2, 2:0, 3:0, 4:0} Number of user invitations per user level
......@@ -78,7 +78,10 @@ ASTAKOS_LOGIN_MESSAGES {}
e.g. {'warning': 'Warning message (can contain html)'}
ASTAKOS_PROFILE_EXTRA_LINKS {} messages to display as extra actions in account forms
e.g. {'https://cms.okeanos.grnet.gr/': 'Back to ~okeanos'}
============================== ============================================================================= ===========================================================================================
ASTAKOS_RATELIMIT_RETRIES_ALLOWED 3 Number of unsuccessful login requests allowed for a specific account.
When this number exceeds and ASTAKOS_RECAPTCHA_ENABLED is set the user has to solve a
captcha challenge.
================================= ============================================================================= ===========================================================================================
Administrator functions
-----------------------
......
......@@ -208,9 +208,7 @@ class SimpleBackend(SignupBackend):
if request.method == 'POST':
if provider == request.POST.get('provider', ''):
initial_data = request.POST
ip = self.request.META.get('REMOTE_ADDR',
self.request.META.get('HTTP_X_REAL_IP', None))
return globals()[formclass](initial_data, instance=instance, ip=ip)
return globals()[formclass](initial_data, instance=instance, request=request)
def _is_preaccepted(self, user):
if super(SimpleBackend, self)._is_preaccepted(user):
......
......@@ -76,9 +76,12 @@ class LocalUserCreationForm(UserCreationForm):
"""
Changes the order of fields, and removes the username field.
"""
if 'ip' in kwargs:
self.ip = kwargs['ip']
kwargs.pop('ip')
request = kwargs.get('request', None)
if request:
kwargs.pop('request')
self.ip = request.META.get('REMOTE_ADDR',
request.META.get('HTTP_X_REAL_IP', None))
super(LocalUserCreationForm, self).__init__(*args, **kwargs)
self.fields.keyOrder = ['email', 'first_name', 'last_name',
'password1', 'password2']
......@@ -186,8 +189,6 @@ class ThirdPartyUserCreationForm(forms.ModelForm):
"""
Changes the order of fields, and removes the username field.
"""
if 'ip' in kwargs:
kwargs.pop('ip')
super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
self.fields.keyOrder = ['email', 'first_name', 'last_name',
'provider', 'third_party_identifier']
......@@ -284,6 +285,43 @@ class InvitedShibbolethUserCreationForm(InvitedThirdPartyUserCreationForm):
class LoginForm(AuthenticationForm):
username = forms.EmailField(label=_("Email"))
recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
recaptcha_response_field = forms.CharField(widget=RecaptchaWidget, label='')
def __init__(self, *args, **kwargs):
was_limited = kwargs.get('was_limited', False)
request = kwargs.get('request', None)
if request:
self.ip = request.META.get('REMOTE_ADDR',
request.META.get('HTTP_X_REAL_IP', None))
t = ('request', 'was_limited')
for elem in t:
if elem in kwargs.keys():
kwargs.pop(elem)
super(LoginForm, self).__init__(*args, **kwargs)
self.fields.keyOrder = ['username', 'password']
if was_limited and RECAPTCHA_ENABLED:
self.fields.keyOrder.extend(['recaptcha_challenge_field',
'recaptcha_response_field',])
def clean_recaptcha_response_field(self):
if 'recaptcha_challenge_field' in self.cleaned_data:
self.validate_captcha()
return self.cleaned_data['recaptcha_response_field']
def clean_recaptcha_challenge_field(self):
if 'recaptcha_response_field' in self.cleaned_data:
self.validate_captcha()
return self.cleaned_data['recaptcha_challenge_field']
def validate_captcha(self):
rcf = self.cleaned_data['recaptcha_challenge_field']
rrf = self.cleaned_data['recaptcha_response_field']
check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
if not check.is_valid:
raise forms.ValidationError(_('You have not entered the correct words'))
class ProfileForm(forms.ModelForm):
"""
......
......@@ -80,3 +80,6 @@ LOGIN_MESSAGES = getattr(settings, 'ASTAKOS_LOGIN_MESSAGES', {})
# e.g. {'https://cms.okeanos.grnet.gr/': 'Back to ~okeanos'}
PROFILE_EXTRA_LINKS = getattr(settings, 'ASTAKOS_PROFILE_EXTRA_LINKS', {})
# The number of unsuccessful login requests per minute allowed for a specific email
RATELIMIT_RETRIES_ALLOWED = getattr(settings, 'ASTAKOS_RATELIMIT_RETRIES_ALLOWED', 3)
......@@ -42,13 +42,21 @@ from astakos.im.util import prepare_response
from astakos.im.views import requires_anonymous
from astakos.im.models import AstakosUser
from astakos.im.forms import LoginForm
from astakos.im.settings import RATELIMIT_RETRIES_ALLOWED
from ratelimit.decorators import ratelimit
retries = RATELIMIT_RETRIES_ALLOWED-1
rate = str(retries)+'/m'
@requires_anonymous
@ratelimit(field='username', method='POST', rate=rate)
def login(request, on_failure='im/login.html'):
"""
on_failure: the template name to render on login failure
"""
form = LoginForm(data=request.POST)
was_limited = getattr(request, 'limited', False)
form = LoginForm(data=request.POST, was_limited=was_limited, request=request)
next = request.POST.get('next')
if not form.is_valid():
return render_to_response(on_failure,
......
......@@ -84,7 +84,7 @@ def login(request, backend=None, on_login_template='im/login.html', on_creation
message = _('Inactive account')
messages.add_message(request, messages.ERROR, message)
return render_response(on_login_template,
login_form = LoginForm(),
login_form = LoginForm(request=request),
context_instance=RequestContext(request))
except AstakosUser.DoesNotExist, e:
user = AstakosUser(third_party_identifier=eppn, realname=realname,
......
......@@ -135,7 +135,7 @@ def index(request, login_template_name='im/login.html', profile_template_name='i
if request.user.is_authenticated():
return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
return render_response(template_name,
login_form = LoginForm(),
login_form = LoginForm(request=request),
context_instance = get_context(request, extra_context))
@login_required
......
......@@ -79,7 +79,8 @@ INSTALL_REQUIRES = [
'South>=0.7, <=0.7.3',
'httplib2>=0.6.0',
'snf-common>=0.9.0',
'recaptcha-client>=1.0.5'
'recaptcha-client>=1.0.5',
'django-ratelimit==0.1'
]
EXTRAS_REQUIRES = {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment