From cc7aa5ce6daf923ac2461738d80494a58291e412 Mon Sep 17 00:00:00 2001
From: Paul Marillonnet <pmarillonnet@entrouvert.com>
Date: Tue, 20 Dec 2022 11:31:38 +0100
Subject: [PATCH 4/5] views: handle phone input on pw reset view (#69890)

---
 src/authentic2/views.py | 130 ++++++++++++++++++++++++++++------------
 1 file changed, 92 insertions(+), 38 deletions(-)

diff --git a/src/authentic2/views.py b/src/authentic2/views.py
index c08c14179..545cbf795 100644
--- a/src/authentic2/views.py
+++ b/src/authentic2/views.py
@@ -64,13 +64,13 @@ from .a2_rbac.utils import get_default_ou
 from .forms import passwords as passwords_forms
 from .forms import profile as profile_forms
 from .forms import registration as registration_forms
-from .models import Lock
+from .models import Lock, SMSCode
 from .utils import crypto, hooks
 from .utils import misc as utils_misc
 from .utils import switch_user as utils_switch_user
 from .utils.evaluate import make_condition_context
 from .utils.service import get_service, set_home_url
-from .utils.sms import SMSError, send_registration_sms, sms_ratelimit_key
+from .utils.sms import SMSError, generate_code, send_registration_sms, sms_ratelimit_key
 from .utils.view_decorators import enable_view_restriction
 from .utils.views import csrf_token_check
 
@@ -843,9 +843,13 @@ class PasswordResetView(FormView):
 
     form_class = passwords_forms.PasswordResetForm
     title = _('Password Reset')
+    code = None
 
     def get_success_url(self):
-        return reverse('password_reset_instructions')
+        if not app_settings.A2_ACCEPT_PHONE_AUTHENTICATION or not self.code:  # user input is email
+            return reverse('password_reset_instructions')
+        else:  # user input is phone number
+            return reverse('input_registration_code', kwargs={'token': self.code.url_token})
 
     def get_template_names(self):
         return [
@@ -877,6 +881,7 @@ class PasswordResetView(FormView):
             )
         email_field = 'email_or_username' if app_settings.A2_USER_CAN_RESET_PASSWORD_BY_USERNAME else 'email'
         email = form.cleaned_data.get(email_field)
+        phone = form.cleaned_data.get('phone')
 
         # if an email has already been sent, warn once before allowing resend
         token = models.Token.objects.filter(
@@ -896,41 +901,90 @@ class PasswordResetView(FormView):
             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,
-        ):
-            self.request.journal.record('user.password.reset.failure', email=email)
-            form.add_error(
-                email_field,
-                _(
-                    'Multiple emails have already been sent to this address. Further attempts are blocked,'
-                    ' please check your spam folder or try again later.'
-                ),
-            )
-            return self.form_invalid(form)
-        if is_ratelimited(
-            self.request,
-            key='ip',
-            group='pw-reset-email',
-            rate=app_settings.A2_EMAILS_IP_RATELIMIT,
-            increment=True,
-        ):
-            self.request.journal.record('user.password.reset.failure', email=email)
-            form.add_error(
-                email_field,
-                _(
-                    'Multiple password reset attempts have already been made from this IP address. No further'
-                    ' email will be sent, please check your spam folder or try again later.'
-                ),
-            )
-            return self.form_invalid(form)
+        if email:
+            if is_ratelimited(
+                self.request,
+                key='post:email',
+                group='pw-reset-email',
+                rate=app_settings.A2_EMAILS_ADDRESS_RATELIMIT,
+                increment=True,
+            ):
+                self.request.journal.record('user.password.reset.failure', email=email)
+                form.add_error(
+                    email_field,
+                    _(
+                        'Multiple emails have already been sent to this address. Further attempts are blocked,'
+                        ' please check your spam folder or try again later.'
+                    ),
+                )
+                return self.form_invalid(form)
+            if is_ratelimited(
+                self.request,
+                key='ip',
+                group='pw-reset-email',
+                rate=app_settings.A2_EMAILS_IP_RATELIMIT,
+                increment=True,
+            ):
+                self.request.journal.record('user.password.reset.failure', email=email)
+                form.add_error(
+                    email_field,
+                    _(
+                        'Multiple password reset attempts have already been made from this IP address. No further'
+                        ' email will be sent, please check your spam folder or try again later.'
+                    ),
+                )
+                return self.form_invalid(form)
+            form.save()
 
-        form.save()
-        self.request.session['reset_email'] = email
+        elif phone:
+            if is_ratelimited(
+                self.request,
+                key=sms_ratelimit_key,
+                group='pw-reset-sms',
+                rate=app_settings.A2_SMS_NUMBER_RATELIMIT,
+                increment=True,
+            ):
+                form.add_error(
+                    'phone',
+                    _(
+                        'Multiple SMSs have already been sent to this number. Further attempts are blocked,'
+                        ' try again later.'
+                    ),
+                )
+                return self.form_invalid(form)
+            if is_ratelimited(
+                self.request,
+                key='ip',
+                group='pw-reset-sms',
+                rate=app_settings.A2_SMS_IP_RATELIMIT,
+                increment=True,
+            ):
+                form.add_error(
+                    'email',
+                    _(
+                        'Multiple registration attempts have already been made from this IP address. No further'
+                        ' SMS will be sent for now, try again later.'
+                    ),
+                )
+                return self.form_invalid(form)
+
+            self.code, legitimate_request = form.save()
+            if not self.code:
+                if legitimate_request:
+                    messages.error(
+                        self.request,
+                        _(
+                            'Something went wrong while trying to send the SMS code to you. '
+                            'Please contact your administrator and try again later.'
+                        ),
+                    )
+                    return utils_misc.redirect(self.request, reverse('auth_homepage'))
+                else:
+                    self.code = generate_code(phone, kind=SMSCode.KIND_PASSWORD_LOST, fake=True)
+        if email:
+            self.request.session['reset_email'] = email
+        elif phone:
+            self.request.session['reset_phone'] = phone
         return super().form_valid(form)
 
 
@@ -1087,7 +1141,7 @@ class BaseRegistrationView(HomeURLMixin, FormView):
         if email:
             return self.perform_email_registration(form, email)
 
-        if settings.A2_ACCEPT_PHONE_AUTHENTICATION:
+        if app_settings.A2_ACCEPT_PHONE_AUTHENTICATION:
             phone = form.cleaned_data.pop('phone')
             return self.perform_phone_registration(form, phone)
 
-- 
2.39.0

