warn users on password change confirmation (#78111) #114

Merged
pmarillonnet merged 1 commits from wip/78111-login-pwd-authn-warn-on-changed-pwd into main 2023-08-17 11:02:04 +02:00
5 changed files with 48 additions and 13 deletions

View File

@ -241,12 +241,15 @@ class PasswordResetMixin(Form):
class NotifyOfPasswordChange:
def save(self, commit=True):
user = super().save(commit=commit)
if user.email:
authn = get_password_authenticator()
if user.email and user.email_verified:
ctx = {
'user': user,
'password': self.cleaned_data['new_password1'],
}
utils_misc.send_templated_mail(user, 'authentic2/password_change', ctx)
elif authn.is_phone_authn_active and (phone := user.phone_identifier) and user.phone_verified_on:
utils_sms.send_password_reset_confirmation_sms(phone, user.ou)
return user

View File

@ -0,0 +1 @@
{% load i18n %}{% blocktrans %}Your password has been changed. If you are not the author of the change request, please contact an administrator.{% endblocktrans %}

View File

@ -84,10 +84,13 @@ def send_sms(phone_number, ou, user=None, template_names=None, context=None, kin
if not isinstance(context, dict):
context = {}
code = generate_code(phone_number, user=user, kind=kind)
if code.fake is True:
return code
context.update({'code': code})
code = None
if kind is not None:
# SMS with a specific action requires generating a code
code = generate_code(phone_number, user=user, kind=kind)
if code.fake is True:
return code
context.update({'code': code})
message = render_plain_text_template_to_string(template_names, context)
@ -101,7 +104,6 @@ def send_sms(phone_number, ou, user=None, template_names=None, context=None, kin
with transaction.atomic():
response = requests.post(url, json=payload, timeout=10)
response.raise_for_status()
code.save()
except RequestException as e:
logger.warning('sms code to %s using %s failed: %s', phone_number, url, e)
raise SMSError(f'Error while contacting SMS service: {e}')
@ -121,6 +123,15 @@ def send_registration_sms(phone_number, ou, template_names=None, context=None, *
)
def send_password_reset_confirmation_sms(phone_number, ou, context=None):
return send_sms(
phone_number,
ou,
template_names=['password_lost/sms_password_change_confirmation.txt'],
context=context,
)
def send_account_deletion_sms(phone_number, ou, user=None, template_names=None, context=None, **kwargs):
from authentic2.models import SMSCode

View File

@ -1319,6 +1319,7 @@ class PasswordResetConfirmView(cbv.RedirectToNextURLViewMixin, FormView):
def dispatch(self, request, *args, **kwargs):
token = kwargs['token'].replace(' ', '')
self.next_url = utils_misc.select_next_url(request, None)
self.authenticator = utils_misc.get_password_authenticator()
try:
self.token = models.Token.use('pw-reset', token, delete=False)
except models.Token.DoesNotExist:
@ -1363,8 +1364,11 @@ class PasswordResetConfirmView(cbv.RedirectToNextURLViewMixin, FormView):
return kwargs
def form_valid(self, form):
# Changing password by mail validate the email
form.user.set_email_verified(True, source='user')
# Changing password by mail validate the user's known identifier
if self.token.content.get('email'):
form.user.set_email_verified(True, source='user')
elif self.token.content.get('phone'):
form.user.phone_verified_on = timezone.now()
form.save()
hooks.call_hooks('event', name='password-reset-confirm', user=form.user, token=self.token, form=form)
logger.info('password reset for user %s with token %r', self.user, self.token.uuid)

View File

@ -56,6 +56,7 @@ def test_send_password_reset_email(app, simple_user, mailoutbox):
resp.form.set('new_password1', '1234==aA')
resp.form.set('new_password2', '1234==aA')
resp = resp.form.submit().follow()
assert len(mailoutbox) == 2
assert str(app.session['_auth_user_id']) == str(simple_user.pk)
utils.assert_event('user.password.reset', user=simple_user, session=app.session)
@ -104,7 +105,10 @@ def test_send_password_reset_by_sms_code(app, nomail_user, settings, phone_activ
assert authenticate(username='user', password='1234==aA') is None
resp.form.set('new_password1', '1234==aA')
resp.form.set('new_password2', '1234==aA')
resp.form.submit()
with HTTMock(sms_service_mock):
resp.form.submit()
assert sms_service_mock.call['count'] == 1
assert SMSCode.objects.count() == 1 # no new code generated
# verify user is logged
assert str(app.session['_auth_user_id']) == str(nomail_user.pk)
user = authenticate(username='user', password='1234==aA')
@ -115,7 +119,9 @@ def test_send_password_reset_by_sms_code(app, nomail_user, settings, phone_activ
app.get(url, status=404)
def test_send_password_reset_by_sms_code_nondefault_attribute(app, nomail_user, simple_user, settings):
def test_send_password_reset_by_sms_code_nondefault_attribute(
app, nomail_user, simple_user, settings, phone_activated_authn
):
phone, dummy = Attribute.objects.get_or_create(
name='another_phone',
kind='phone_number',
@ -142,7 +148,10 @@ def test_send_password_reset_by_sms_code_nondefault_attribute(app, nomail_user,
resp.form.set('new_password1', '1234==aA')
resp.form.set('new_password2', '1234==aA')
resp.form.submit()
with HTTMock(sms_service_mock):
resp.form.submit()
assert sms_service_mock.call['count'] == 1
assert SMSCode.objects.count() == 1 # no new code generated
# verify user is logged
assert str(app.session['_auth_user_id']) == str(nomail_user.pk)
user = authenticate(username='user', password='1234==aA')
@ -167,7 +176,10 @@ def test_send_password_reset_by_sms_code_nondefault_attribute(app, nomail_user,
resp.form.set('new_password1', '1234==aA')
resp.form.set('new_password2', '1234==aA')
resp.form.submit()
with HTTMock(sms_service_mock):
resp.form.submit()
assert sms_service_mock.call['count'] == 1
assert SMSCode.objects.count() == 1 # no new code generated
# verify user is logged
assert str(app.session['_auth_user_id']) == str(simple_user.pk)
user = authenticate(username='simpleuser', password='1234==aA')
@ -194,7 +206,10 @@ def test_send_password_reset_by_sms_code_next_url(app, nomail_user, settings, ph
resp.form.set('new_password1', '1234==aA')
resp.form.set('new_password2', '1234==aA')
resp = resp.form.submit()
with HTTMock(sms_service_mock):
resp = resp.form.submit()
assert sms_service_mock.call['count'] == 1
assert SMSCode.objects.count() == 1 # no new code generated
user = authenticate(username='user', password='1234==aA')
assert user == nomail_user
assert resp.location == '/accounts/consents/'
@ -307,6 +322,7 @@ def test_can_reset_by_username(app, db, simple_user, settings, mailoutbox):
resp.form.set('new_password1', '1234==aA')
resp.form.set('new_password2', '1234==aA')
resp = resp.form.submit()
assert len(mailoutbox) == 2
# verify user is logged
assert str(app.session['_auth_user_id']) == str(simple_user.pk)