views: warn user before generating new token (#41792)

This commit is contained in:
Valentin Deniaud 2020-04-22 16:40:55 +02:00 committed by Benjamin Dauvergne
parent 7d9e65dc96
commit 4f831fe4d8
6 changed files with 66 additions and 6 deletions

View File

@ -335,6 +335,9 @@ default_settings = dict(
A2_USER_DELETED_KEEP_DATA_DAYS=Setting(
default=365,
definition='Number of days to keep data on deleted users'),
A2_TOKEN_EXISTS_WARNING=Setting(
default=True,
definition='If an active token exists, warn user before generating a new one.'),
)
app_settings = AppSettings(default_settings)

View File

@ -804,8 +804,8 @@ def build_reset_password_url(user, request=None, next_url=None, set_random_passw
user.save()
lifetime = settings.PASSWORD_RESET_TIMEOUT_DAYS * 3600 * 24
# invalidate any token associated with this user
Token.objects.filter(kind='pw-reset', content__user=user.pk).delete()
token = Token.create('pw-reset', {'user': user.pk}, duration=lifetime)
Token.objects.filter(kind='pw-reset', content__user=user.pk, content__email=user.email).delete()
token = Token.create('pw-reset', {'user': user.pk, 'email': user.email}, duration=lifetime)
reset_url = make_url(
'password_reset_confirm',
kwargs={'token': token.uuid_b64url},

View File

@ -31,7 +31,7 @@ from django import shortcuts
from django.core import signing
from django.core.exceptions import ValidationError
from django.contrib import messages
from django.utils import six
from django.utils import six, timezone
from django.utils.translation import ugettext as _
from django.urls import reverse
from django.contrib.auth import logout as auth_logout
@ -659,6 +659,23 @@ class PasswordResetView(FormView):
return ctx
def form_valid(self, form):
email = form.cleaned_data['email']
# if an email has already been sent, warn once before allowing resend
token = models.Token.objects.filter(
kind='pw-reset', content__email=email, expires__gt=timezone.now()
).exists()
resend_key = 'pw-reset-allow-resend'
if app_settings.A2_TOKEN_EXISTS_WARNING and token and not self.request.session.get(resend_key):
self.request.session[resend_key] = True
form.add_error(
'email',
_('An email has already been sent to %s. Click "Validate" again if '
'you really want it to be sent again.') % email
)
return self.form_invalid(form)
self.request.session[resend_key] = False
if is_ratelimited(self.request, key='post:email', group='pw-reset-email',
rate=app_settings.A2_EMAILS_ADDRESS_RATELIMIT, increment=True):
form.add_error(
@ -677,7 +694,7 @@ class PasswordResetView(FormView):
return self.form_invalid(form)
form.save()
self.request.session['reset_email'] = form.cleaned_data['email']
self.request.session['reset_email'] = email
return super(PasswordResetView, self).form_valid(form)
password_reset = PasswordResetView.as_view()
@ -791,6 +808,23 @@ class BaseRegistrationView(FormView):
return super(BaseRegistrationView, self).dispatch(request, *args, **kwargs)
def form_valid(self, form):
email = form.cleaned_data.pop('email')
# if an email has already been sent, warn once before allowing resend
token = models.Token.objects.filter(
kind='registration', content__email=email, expires__gt=timezone.now()
).exists()
resend_key = 'registration-allow-resend'
if app_settings.A2_TOKEN_EXISTS_WARNING and token and not self.request.session.get(resend_key):
self.request.session[resend_key] = True
form.add_error(
'email',
_('An email has already been sent to %s. Click "Validate" again if '
'you really want it to be sent again.') % email
)
return self.form_invalid(form)
self.request.session[resend_key] = False
if is_ratelimited(self.request, key='post:email', group='registration-email',
rate=app_settings.A2_EMAILS_ADDRESS_RATELIMIT, increment=True):
form.add_error(
@ -808,7 +842,6 @@ class BaseRegistrationView(FormView):
)
return self.form_invalid(form)
email = form.cleaned_data.pop('email')
for field in form.cleaned_data:
self.token[field] = form.cleaned_data[field]

View File

@ -54,3 +54,5 @@ SITE_BASE_URL = 'http://testserver'
A2_MAX_EMAILS_PER_IP = None
A2_MAX_EMAILS_FOR_ADDRESS = None
A2_TOKEN_EXISTS_WARNING = False

View File

@ -187,7 +187,8 @@ def test_fr_postcode(db, app, admin, mailoutbox):
qs.delete()
def test_phone_number(db, app, admin, mailoutbox):
def test_phone_number(db, app, admin, mailoutbox, settings):
settings.A2_EMAILS_ADDRESS_RATELIMIT = None
def register_john():
response = app.get('/accounts/register/')

View File

@ -215,3 +215,24 @@ def test_views_email_ratelimit(app, db, simple_user, settings, mailoutbox, freez
response.form.set('email', simple_user.email)
response = response.form.submit()
assert len(mailoutbox) == 12
@pytest.mark.parametrize('view_name', ['registration_register', 'password_reset'])
def test_views_email_token_resend(app, simple_user, settings, mailoutbox, view_name):
settings.A2_TOKEN_EXISTS_WARNING = True
response = app.get(reverse(view_name))
response.form.set('email', simple_user.email)
response = response.form.submit()
assert len(mailoutbox) == 1
# warn user token has already been sent
response = app.get(reverse(view_name))
response.form.set('email', simple_user.email)
response = response.form.submit()
assert 'email has already been sent' in response.text
assert len(mailoutbox) == 1
# validating again anyway works
response = response.form.submit()
assert len(mailoutbox) == 2