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