From d1d140fb436efb9e2bc62e466a752cf92f926430 Mon Sep 17 00:00:00 2001
From: Fredrik Jonsson <frjo@xdeb.org>
Date: Wed, 22 May 2019 15:11:36 +0200
Subject: [PATCH] Implement 2fa with django-two-factor-auth.

---
 opentech/apply/urls.py                        |  3 +
 .../users/templates/two_factor/_base.html     |  7 +++
 .../templates/two_factor/_base_focus.html     | 17 ++++++
 .../templates/two_factor/_wizard_actions.html | 17 ++++++
 .../apply/users/templates/users/account.html  |  5 +-
 .../apply/users/templates/users/login.html    | 56 ++++++++++++++++++-
 opentech/apply/users/urls.py                  |  9 +--
 opentech/settings/base.py                     |  5 ++
 .../src/sass/apply/components/_button.scss    |  4 ++
 .../src/sass/apply/components/_link.scss      | 13 ++++-
 .../src/sass/public/components/_button.scss   | 18 +++++-
 .../src/sass/public/components/_link.scss     |  1 +
 opentech/templates/base-apply.html            |  2 +
 requirements.txt                              |  1 +
 14 files changed, 148 insertions(+), 10 deletions(-)
 create mode 100644 opentech/apply/users/templates/two_factor/_base.html
 create mode 100644 opentech/apply/users/templates/two_factor/_base_focus.html
 create mode 100644 opentech/apply/users/templates/two_factor/_wizard_actions.html

diff --git a/opentech/apply/urls.py b/opentech/apply/urls.py
index deb327004..6138117fa 100644
--- a/opentech/apply/urls.py
+++ b/opentech/apply/urls.py
@@ -1,6 +1,8 @@
 from django.conf import settings
 from django.urls import include, path
 
+from two_factor.urls import urlpatterns as tf_urls
+
 from .utils import views
 from .users import urls as users_urls
 from .dashboard import urls as dashboard_urls
@@ -14,6 +16,7 @@ urlpatterns = [
     path('', include(users_urls)),
     path('dashboard/', include(dashboard_urls)),
     path('hijack/', include('hijack.urls', 'hijack')),
+    path('', include(tf_urls, 'two_factor')),
 ]
 
 if settings.DEBUG:
diff --git a/opentech/apply/users/templates/two_factor/_base.html b/opentech/apply/users/templates/two_factor/_base.html
new file mode 100644
index 000000000..dba38b695
--- /dev/null
+++ b/opentech/apply/users/templates/two_factor/_base.html
@@ -0,0 +1,7 @@
+{% extends 'base-apply.html' %}
+
+{% block content_wrapper %}
+    <div class="wrapper wrapper--small wrapper--inner-space-medium">
+        {% block content %}{% endblock %}
+    </div>
+{% endblock %}
diff --git a/opentech/apply/users/templates/two_factor/_base_focus.html b/opentech/apply/users/templates/two_factor/_base_focus.html
new file mode 100644
index 000000000..86888aedb
--- /dev/null
+++ b/opentech/apply/users/templates/two_factor/_base_focus.html
@@ -0,0 +1,17 @@
+{% extends "two_factor/_base.html" %}
+
+{% block content_wrapper %}
+    <div class="admin-bar">
+        <div class="admin-bar__inner admin-bar__inner--with-button">
+            <h3 class="admin-bar__heading">Welcome {{ user }}</h3>
+            <a href="{% url 'dashboard:dashboard' %}" class="button button--primary button--arrow-pixels-white">
+                Go to dashboard
+                <svg><use xlink:href="#arrow-head-pixels--solid"></use></svg>
+            </a>
+        </div>
+    </div>
+
+    <div class="wrapper wrapper--small wrapper--inner-space-medium">
+        {% block content %}{% endblock %}
+    </div>
+{% endblock %}
diff --git a/opentech/apply/users/templates/two_factor/_wizard_actions.html b/opentech/apply/users/templates/two_factor/_wizard_actions.html
new file mode 100644
index 000000000..cd01d7d84
--- /dev/null
+++ b/opentech/apply/users/templates/two_factor/_wizard_actions.html
@@ -0,0 +1,17 @@
+{% load i18n %}
+
+{% if wizard.steps.prev %}
+  <button name="wizard_goto_step" type="submit"
+          value="{{ wizard.steps.prev }}"
+          class="button button--primary">{% trans "Back" %}</button>
+{% else %}
+  <button disabled name="" type="button"
+          class="button button--primary">{% trans "Back" %}</button>
+{% endif %}
+
+<button type="submit" class="button button--primary">{% trans "Next" %}</button>
+
+{% if cancel_url %}
+  <a href="{% url 'users:account' %}"
+     class="link link--bold link--left-space">{% trans "Cancel" %}</a>
+{% endif %}
diff --git a/opentech/apply/users/templates/users/account.html b/opentech/apply/users/templates/users/account.html
index 6156b72af..225527bae 100644
--- a/opentech/apply/users/templates/users/account.html
+++ b/opentech/apply/users/templates/users/account.html
@@ -29,7 +29,10 @@
     {% if show_change_password and user.has_usable_password and not backends.associated %}
         <div class="profile__column">
             <h3>Change password</h3>
-            <a class="button button--primary" href="{% url 'users:password_change' %}">{% trans "Update password" %}</a>
+            <p><a class="button button--primary" href="{% url 'users:password_change' %}">{% trans "Update password" %}</a></p>
+
+            <h3>Account Security</h3>
+            <p><a class="link link--button link--button--narrow" href="{% url 'two_factor:profile' %}">Two-factor authentication settings</a></p>
         </div>
     {% endif %}
 
diff --git a/opentech/apply/users/templates/users/login.html b/opentech/apply/users/templates/users/login.html
index 73d84763c..e21c21092 100644
--- a/opentech/apply/users/templates/users/login.html
+++ b/opentech/apply/users/templates/users/login.html
@@ -1,15 +1,65 @@
 {% extends 'base.html' %}
+{% load i18n two_factor %}
+
 {% block header_modifier %}header--light-bg{% endblock %}
 {% block page_title %}Login{% endblock %}
 {% block title %}Login{% endblock %}
 
 {% block content %}
   <div class="wrapper wrapper--small">
+    {% if wizard.steps.current == 'auth' %}
+        <p>{% blocktrans %}Enter your credentials.{% endblocktrans %}</p>
+    {% elif wizard.steps.current == 'token' %}
+        {% if device.method == 'call' %}
+            <p>{% blocktrans %}We are calling your phone right now, please enter the
+                digits you hear.{% endblocktrans %}</p>
+        {% elif device.method == 'sms' %}
+            <p>{% blocktrans %}We sent you a text message, please enter the tokens we
+                sent.{% endblocktrans %}</p>
+        {% else %}
+            <p>{% blocktrans %}Please enter the tokens generated by your token
+                generator.{% endblocktrans %}</p>
+        {% endif %}
+    {% elif wizard.steps.current == 'backup' %}
+        <p>{% blocktrans %}Use this form for entering backup tokens for logging in.
+            These tokens have been generated for you to print and keep safe. Please
+            enter one of these backup tokens to login to your account.{% endblocktrans %}</p>
+    {% endif %}
+
     <form class="form form--with-p-tags" method="post">
       {% csrf_token %}
-      {{ form.as_p }}
-      <p><a class="link link--small" href="{% url 'users:password_reset' %}">Forgot your password?</a></p>
-      <button class="link link--button-secondary" type="submit">Login</button>
+      {{ wizard.management_form }}
+
+      {% if wizard.steps.current == 'auth' %}
+        {{ form.as_p }}
+        <p><a class="link link--small" href="{% url 'users:password_reset' %}">Forgot your password?</a></p>
+        <button class="link link--button-secondary" type="submit">Login</button>
+      {% else %}
+        {{ wizard.form }}
+
+        {# hidden submit button to enable [enter] key #}
+        <div style="margin-left: -9999px"><input type="submit" value=""/></div>
+
+        {% if other_devices %}
+            <p>{% trans "Or, alternatively, use one of your backup phones:" %}</p>
+            <p>
+                {% for other in other_devices %}
+                <button name="challenge_device" value="{{ other.persistent_id }}"
+                                class="btn btn-default btn-block" type="submit">
+                    {{ other|device_action }}
+                </button>
+            {% endfor %}</p>
+        {% endif %}
+        {% if backup_tokens %}
+            <p>{% trans "As a last resort, you can use a backup token:" %}</p>
+            <p>
+                <button name="wizard_goto_step" type="submit" value="backup"
+                                class="btn btn-default btn-block">{% trans "Use Backup Token" %}</button>
+            </p>
+        {% endif %}
+
+        {% include "two_factor/_wizard_actions.html" %}
+      {% endif %}
     </form>
 
     <div class="wrapper wrapper--inner-space-large">
diff --git a/opentech/apply/users/urls.py b/opentech/apply/users/urls.py
index 02e7899ca..acfc6fafc 100644
--- a/opentech/apply/users/urls.py
+++ b/opentech/apply/users/urls.py
@@ -2,6 +2,8 @@ from django.urls import path, include
 from django.contrib.auth import views as auth_views
 from django.urls import reverse_lazy
 
+from two_factor.views import LoginView
+
 from opentech.apply.users.views import AccountView, become, oauth, ActivationView, create_password
 
 
@@ -11,9 +13,8 @@ app_name = 'users'
 public_urlpatterns = [
     path(
         'login/',
-        auth_views.LoginView.as_view(
-            template_name='users/login.html',
-            redirect_authenticated_user=True
+        LoginView.as_view(
+            template_name='users/login.html'
         ),
         name='login'
     ),
@@ -73,5 +74,5 @@ urlpatterns = [
         ),
         path('activate/', create_password, name="activate_password"),
         path('oauth', oauth, name='oauth'),
-    ]))
+    ])),
 ]
diff --git a/opentech/settings/base.py b/opentech/settings/base.py
index bef78cd42..edf98f4fb 100644
--- a/opentech/settings/base.py
+++ b/opentech/settings/base.py
@@ -117,6 +117,10 @@ INSTALLED_APPS = [
     'django_bleach',
     'django_fsm',
     'django_pwned_passwords',
+    'django_otp',
+    'django_otp.plugins.otp_totp',
+    'django_otp.plugins.otp_static',
+    'two_factor',
     'rest_framework',
     'wagtailcache',
 
@@ -147,6 +151,7 @@ MIDDLEWARE = [
     'django.contrib.messages.middleware.MessageMiddleware',
     'django.middleware.clickjacking.XFrameOptionsMiddleware',
     'django_referrer_policy.middleware.ReferrerPolicyMiddleware',
+    'django_otp.middleware.OTPMiddleware',
 
     'opentech.apply.users.middleware.SocialAuthExceptionMiddleware',
 
diff --git a/opentech/static_src/src/sass/apply/components/_button.scss b/opentech/static_src/src/sass/apply/components/_button.scss
index bec7e6d46..9d7ebc203 100644
--- a/opentech/static_src/src/sass/apply/components/_button.scss
+++ b/opentech/static_src/src/sass/apply/components/_button.scss
@@ -1,3 +1,4 @@
+.btn,
 .button {
     padding: 0;
     background-color: transparent;
@@ -15,6 +16,8 @@
         opacity: .5;
     }
 
+    &-default,
+    &-primary,
     &--primary {
         @include button($color--light-blue, $color--dark-blue);
         display: inline-block;
@@ -65,6 +68,7 @@
         }
     }
 
+    &-danger,
     &--warning {
         @include button($color--error, $color--error);
 
diff --git a/opentech/static_src/src/sass/apply/components/_link.scss b/opentech/static_src/src/sass/apply/components/_link.scss
index 91abab5e1..35e89799f 100644
--- a/opentech/static_src/src/sass/apply/components/_link.scss
+++ b/opentech/static_src/src/sass/apply/components/_link.scss
@@ -9,7 +9,6 @@
         @include button($color--light-blue, $color--dark-blue);
         display: inline-block;
 
-
         &--narrow {
             @include button--narrow;
         }
@@ -27,6 +26,10 @@
         font-weight: $weight--bold;
     }
 
+    &--left-space {
+        margin-left: 20px;
+    }
+
     &--download {
         display: flex;
         align-items: center;
@@ -91,6 +94,14 @@
         }
     }
 
+    &--button-long-text {
+        padding: 10px;
+
+        @include media-query(tablet-portrait) {
+            padding: 10px 60px;
+        }
+    }
+
     &--open-feed {
         position: fixed;
         right: 20px;
diff --git a/opentech/static_src/src/sass/public/components/_button.scss b/opentech/static_src/src/sass/public/components/_button.scss
index a2962ae26..8f93edac1 100644
--- a/opentech/static_src/src/sass/public/components/_button.scss
+++ b/opentech/static_src/src/sass/public/components/_button.scss
@@ -9,6 +9,23 @@
         cursor: pointer;
     }
 
+    &:disabled,
+    &.is-disabled {
+        pointer-events: none;
+        opacity: .5;
+    }
+
+    &--primary {
+        @include button($color--light-blue, $color--dark-blue);
+        display: inline-block;
+
+        .form--filters & {
+            width: 100%;
+            text-align: center;
+            height: 45px;
+        }
+    }
+
     &--left-space {
         margin-left: 20px;
     }
@@ -111,4 +128,3 @@
         }
     }
 }
-
diff --git a/opentech/static_src/src/sass/public/components/_link.scss b/opentech/static_src/src/sass/public/components/_link.scss
index 6bb2da46d..7d4790b20 100644
--- a/opentech/static_src/src/sass/public/components/_link.scss
+++ b/opentech/static_src/src/sass/public/components/_link.scss
@@ -36,6 +36,7 @@
         @include button(transparent, $color--darkest-blue);
         color: $color--white;
 
+        &:focus,
         &:hover {
             border: 1px solid transparent;
         }
diff --git a/opentech/templates/base-apply.html b/opentech/templates/base-apply.html
index 3d45584da..3b304836d 100644
--- a/opentech/templates/base-apply.html
+++ b/opentech/templates/base-apply.html
@@ -95,7 +95,9 @@
         </header>
 
         <main class="wrapper wrapper--large wrapper--main">
+            {% block content_wrapper %}
             {% block content %}{% endblock %}
+            {% endblock %}
         </main>
 
         <footer class="footer"></footer>
diff --git a/requirements.txt b/requirements.txt
index ec3bc5337..46114ac41 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -33,6 +33,7 @@ django-referrer-policy==1.0
 django-storages==1.6.6
 django-tables2==1.21.1
 django-tinymce4-lite==1.7.0
+django-two-factor-auth==1.8.0
 django-webpack-loader==0.6.0
 django_select2==6.0.1
 djangorestframework==3.9.0
-- 
GitLab