From 5ef94e4c7f6ba7f4698b7ab07121c9dd5e852911 Mon Sep 17 00:00:00 2001
From: Fredrik Jonsson <frjo@xdeb.org>
Date: Fri, 10 Jun 2022 11:12:29 +0200
Subject: [PATCH] Update django-two-factor-auth and add otpauth link to setup
 view.

---
 .../templates/two_factor/core/setup.html      |  9 +++--
 .../templates/two_factor/profile/profile.html | 10 +++---
 hypha/apply/users/templates/users/login.html  |  4 +--
 hypha/apply/users/urls.py                     |  2 ++
 hypha/apply/users/views.py                    | 33 ++++++++++++++++++-
 requirements.txt                              |  2 +-
 6 files changed, 46 insertions(+), 14 deletions(-)

diff --git a/hypha/apply/users/templates/two_factor/core/setup.html b/hypha/apply/users/templates/two_factor/core/setup.html
index 4eb1cf4f7..76824d092 100644
--- a/hypha/apply/users/templates/two_factor/core/setup.html
+++ b/hypha/apply/users/templates/two_factor/core/setup.html
@@ -2,11 +2,11 @@
 {% load i18n %}
 
 {% block content %}
-  <h1>{% block title %}{% trans "Two-Factor Authentication(2FA)" %}{% endblock %}</h1>
+  <h1>{% block title %}{% trans "Two-Factor Authentication (2FA)" %}{% endblock %}</h1>
   {% if wizard.steps.current == 'welcome' %}
     <p>{% blocktrans trimmed %}You are about to take your account security to the
         next level.{% endblocktrans %}</p>
-    <p>{% blocktrans trimmed %}To start using 2FA, you need to install an Authenticator app(eg. Google Authenticator app) on your smartphone.{% endblocktrans %}</p>
+    <p>{% blocktrans trimmed %}To start using 2FA, you need to install an Authenticator app on your smartphone or computer. With Safari on Apple devices you can also use a built in system.{% endblocktrans %}</p>
     <p>{% blocktrans trimmed %}Install the app you choose then continue to Enable Two-Factor Authentication. {% endblocktrans %}</p>
   {% elif wizard.steps.current == 'method' %}
     <p>{% blocktrans trimmed %}Please select which authentication method you would
@@ -16,7 +16,8 @@
         smartphone to scan the QR code below. For example, use Google
         Authenticator. Then, enter the token generated by the app.
         {% endblocktrans %}</p>
-    <p><img src="{{ QR_URL }}" alt="QR Code" /></p>
+    <p><img src="{{ QR_URL }}" alt="QR Code" class="bg-white" width="200" height="200" /></p>
+    <p><a href="{{ otpauth_url }}">Alternative otpauth set up link</a></p>
   {% elif wizard.steps.current == 'sms' %}
     <p>{% blocktrans trimmed %}Please enter the phone number you wish to receive the
       text messages on. This number will be validated in the next step.
@@ -49,8 +50,6 @@
   <form action="" method="post">{% csrf_token %}
     {% include "two_factor/_wizard_forms.html" %}
 
-    <!-- Add empty p tag to add some space in between wizard form fields and wizard actions buttons. -->
-    <p></p>
     {# hidden submit button to enable [enter] key #}
     <input type="submit" value="" class="d-none" />
 
diff --git a/hypha/apply/users/templates/two_factor/profile/profile.html b/hypha/apply/users/templates/two_factor/profile/profile.html
index b419947de..fc7177d1c 100644
--- a/hypha/apply/users/templates/two_factor/profile/profile.html
+++ b/hypha/apply/users/templates/two_factor/profile/profile.html
@@ -1,5 +1,5 @@
 {% extends "two_factor/_base.html" %}
-{% load i18n two_factor %}
+{% load i18n %}
 
 {% block content %}
   <p><a href="{% url 'users:account' %}" class="btn btn-primary">
@@ -12,7 +12,7 @@
     {% if default_device_type == 'TOTPDevice' %}
       <p>{% trans "Tokens will be generated by your token generator." %}</p>
     {% elif default_device_type == 'PhoneDevice' %}
-      <p>{% blocktrans with primary=default_device|device_action %}Primary method: {{ primary }}{% endblocktrans %}</p>
+      <p>{% blocktrans with primary=default_device.generate_challenge_button_title %}Primary method: {{ primary }}{% endblocktrans %}</p>
     {% elif default_device_type == 'RemoteYubikeyDevice' %}
       <p>{% blocktrans %}Tokens will be generated by your YubiKey.{% endblocktrans %}</p>
     {% endif %}
@@ -24,11 +24,11 @@
       <ul>
         {% for phone in backup_phones %}
           <li>
-            {{ phone|device_action }}
+            {{ phone.generate_challenge_button_title }}
             <form method="post" action="{% url 'two_factor:phone_delete' phone.id %}"
-                  onsubmit="return confirm('Are you sure?')">
+                  onsubmit="return confirm({% trans 'Are you sure?' %})">
               {% csrf_token %}
-              <button class="btn btn-xs btn-warning"
+              <button class="btn btn-sm btn-warning"
                       type="submit">{% trans "Unregister" %}</button>
             </form>
           </li>
diff --git a/hypha/apply/users/templates/users/login.html b/hypha/apply/users/templates/users/login.html
index 2468c63e8..be26db3d2 100644
--- a/hypha/apply/users/templates/users/login.html
+++ b/hypha/apply/users/templates/users/login.html
@@ -1,5 +1,5 @@
 {% extends 'base.html' %}
-{% load i18n two_factor wagtailcore_tags %}
+{% load i18n wagtailcore_tags %}
 
 {% block header_modifier %}header--light-bg{% endblock %}
 {% block page_title %}Login{% endblock %}
@@ -51,7 +51,7 @@
                 {% for other in other_devices %}
                 <button name="challenge_device" value="{{ other.persistent_id }}"
                                 class="btn btn-default btn-block" type="submit">
-                    {{ other|device_action }}
+                    {{ other.generate_challenge_button_title }}
                 </button>
             {% endfor %}</p>
         {% endif %}
diff --git a/hypha/apply/users/urls.py b/hypha/apply/users/urls.py
index 2fd5ff936..ec4e26546 100644
--- a/hypha/apply/users/urls.py
+++ b/hypha/apply/users/urls.py
@@ -13,6 +13,7 @@ from .views import (
     TWOFABackupTokensPasswordView,
     TWOFADisableView,
     TWOFARequiredMessageView,
+    TWOFASetupView,
     become,
     create_password,
     oauth,
@@ -87,6 +88,7 @@ urlpatterns = [
         ),
         # Two factor redirect
         path('two_factor/required/', TWOFARequiredMessageView.as_view(), name='two_factor_required'),
+        path('two_factor/setup/', TWOFASetupView.as_view(), name='setup'),
         path('two_factor/backup_tokens/password/', TWOFABackupTokensPasswordView.as_view(), name='backup_tokens_password'),
         path('two_factor/disable/', TWOFADisableView.as_view(), name='disable'),
         path('two_factor/admin/disable/<str:user_id>/', TWOFAAdminDisableView.as_view(), name='admin_disable'),
diff --git a/hypha/apply/users/views.py b/hypha/apply/users/views.py
index e3a3dc9aa..eb896769a 100644
--- a/hypha/apply/users/views.py
+++ b/hypha/apply/users/views.py
@@ -7,6 +7,7 @@ from django.contrib.auth import get_user_model, login, update_session_auth_hash
 from django.contrib.auth.decorators import login_required, permission_required
 from django.contrib.auth.forms import AdminPasswordChangeForm
 from django.contrib.auth.tokens import PasswordResetTokenGenerator
+from django.contrib.sites.shortcuts import get_current_site
 from django.core.exceptions import PermissionDenied
 from django.core.signing import BadSignature, Signer, TimestampSigner, dumps, loads
 from django.shortcuts import Http404, get_object_or_404, redirect, render
@@ -16,16 +17,18 @@ from django.utils.decorators import method_decorator
 from django.utils.encoding import force_str
 from django.utils.http import urlsafe_base64_decode
 from django.utils.translation import gettext_lazy as _
+from django.views.decorators.cache import never_cache
 from django.views.generic import UpdateView
 from django.views.generic.base import TemplateView
 from django.views.generic.edit import FormView
 from django_otp import devices_for_user
 from hijack.views import AcquireUserView
 from two_factor.forms import AuthenticationTokenForm, BackupTokenForm
-from two_factor.utils import default_device
+from two_factor.utils import default_device, get_otpauth_url, totp_digits
 from two_factor.views import BackupTokensView as TwoFactorBackupTokensView
 from two_factor.views import DisableView as TwoFactorDisableView
 from two_factor.views import LoginView as TwoFactorLoginView
+from two_factor.views import SetupView as TwoFactorSetupView
 from wagtail.admin.views.account import password_management_enabled
 from wagtail.core.models import Site
 from wagtail.users.views.users import change_user_perm
@@ -284,6 +287,34 @@ def create_password(request):
     })
 
 
+@method_decorator(never_cache, name='dispatch')
+@method_decorator(login_required, name='dispatch')
+class TWOFASetupView(TwoFactorSetupView):
+    def get_issuer(self):
+        return get_current_site(self.request).name
+
+    def get_context_data(self, form, **kwargs):
+        context = super().get_context_data(form, **kwargs)
+        if self.steps.current == 'generator':
+            try:
+                username = self.request.user.get_username()
+            except AttributeError:
+                username = self.request.user.username
+
+            otpauth_url = get_otpauth_url(accountname=username,
+                                          issuer=self.get_issuer(),
+                                          secret=context['secret_key'],
+                                          digits=totp_digits())
+            import logging
+            logger = logging.getLogger('hypha')
+            logger.debug(otpauth_url)
+            context.update({
+                'otpauth_url': otpauth_url,
+            })
+
+        return context
+
+
 @method_decorator(login_required, name='dispatch')
 class TWOFABackupTokensPasswordView(TwoFactorBackupTokensView):
     """
diff --git a/requirements.txt b/requirements.txt
index ae395c233..c66d15403 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -27,7 +27,7 @@ django-select2==7.9.0
 django-storages==1.12.3
 django-tables2==2.4.1
 django-tinymce==3.4.0
-django-two-factor-auth==1.13.1
+django-two-factor-auth==1.14.0
 django-webpack-loader==1.0.0
 django==3.2.13
 djangorestframework-api-key==2.1.0
-- 
GitLab