diff --git a/opentech/apply/urls.py b/opentech/apply/urls.py index deb32700492d24ef9fe3db8d49680f0241fcb757..6138117fab04d25e24a59884ba270f81a3b7f2ec 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 0000000000000000000000000000000000000000..dba38b69559ddce0e130d93f37dc4717390e801d --- /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 0000000000000000000000000000000000000000..86888aedb30617d625d0718f7f1f91000f438c87 --- /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 0000000000000000000000000000000000000000..cd01d7d849245b50d3f784cb8ae88ad2cb099b64 --- /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 6156b72af9b21fcf4911d151201ffd5c8a16cf0b..225527bae2a77e6eedc6b275445871af74ac6403 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 73d84763c32356982ca9135d8ca691e1523a701b..e21c2109296b77f34e04dcc3d2ef0c889858f42d 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 02e7899ca76bf89522a1b0f4d7c153033b404fcf..acfc6fafceb44e0d9d6742c4ac608e5ff602a4fa 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 bef78cd4291e5fbeb0cb6f331de95aaaaa24a279..edf98f4fbc862bef0c067a60687a080146707255 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 bec7e6d46bdeb2b9c51bc7841e5cc7262613adb7..9d7ebc20301a487cd00ae7cd13ea324a02356687 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 91abab5e1aa6dd3410bbc2015725b4239889dc1f..35e89799fd7bd152bf6f498ca47b9ac1b828e1aa 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 a2962ae269586c7b310d19808157261ce7189a9a..8f93edac191f8d041dcd43ffcbc2f753dc15ef42 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 6bb2da46d5339c226f1507f256fdaf66dfc8792c..7d4790b203e0c1384fb609e97c9e5773a3978670 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 3d45584da4fb98941b2519b473070bf5deeefca6..3b304836dd251a87ca9e3ba70c56a92eeeb3537e 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 ec3bc53376c6f238efb19d9ffea89edd0134b4e9..46114ac411cf678fc445e238b48bcc70e3d829df 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