From c9c5ec861226bc49ad5325191418eb60eadac754 Mon Sep 17 00:00:00 2001 From: Markus Holtermann Date: Thu, 28 Jul 2016 00:58:05 +0200 Subject: [PATCH 1/6] Consider passed context data in ProfileView --- two_factor/views/profile.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/two_factor/views/profile.py b/two_factor/views/profile.py index 280729822..f3ef8a4a4 100644 --- a/two_factor/views/profile.py +++ b/two_factor/views/profile.py @@ -23,17 +23,19 @@ class ProfileView(TemplateView): template_name = 'two_factor/profile/profile.html' def get_context_data(self, **kwargs): + context = super(ProfileView, self).get_context_data(**kwargs) try: backup_tokens = self.request.user.staticdevice_set.all()[0].token_set.count() except Exception: backup_tokens = 0 - return { + context.update({ 'default_device': default_device(self.request.user), 'default_device_type': default_device(self.request.user).__class__.__name__, 'backup_phones': backup_phones(self.request.user), 'backup_tokens': backup_tokens, - } + }) + return context @class_view_decorator(never_cache) From 5e2bda1747840a477180d5f8d10686f77ee11110 Mon Sep 17 00:00:00 2001 From: Markus Holtermann Date: Thu, 28 Jul 2016 01:37:16 +0200 Subject: [PATCH 2/6] Moved redirect URLs in login and setup view into methods --- two_factor/views/core.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/two_factor/views/core.py b/two_factor/views/core.py index 52e2e180b..ed97fdc29 100644 --- a/two_factor/views/core.py +++ b/two_factor/views/core.py @@ -81,6 +81,7 @@ def has_backup_step(self): 'backup': has_backup_step, } redirect_field_name = REDIRECT_FIELD_NAME + redirect_url = settings.LOGIN_REDIRECT_URL def __init__(self, **kwargs): super(LoginView, self).__init__(**kwargs) @@ -104,9 +105,7 @@ def done(self, form_list, **kwargs): """ login(self.request, self.get_user()) - redirect_to = self.request.GET.get(self.redirect_field_name, '') - if not is_safe_url(url=redirect_to, host=self.request.get_host()): - redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL) + redirect_to = self.get_redirect_url() device = getattr(self.get_user(), 'otp_device', None) if device: @@ -145,6 +144,12 @@ def get_device(self, step=None): self.device_cache = default_device(self.get_user()) return self.device_cache + def get_redirect_url(self): + redirect_to = self.request.GET.get(self.redirect_field_name, '') + if not is_safe_url(url=redirect_to, host=self.request.get_host()): + redirect_to = resolve_url(self.redirect_url) + return redirect_to + def render(self, form=None, **kwargs): """ If the user selected a device, ask the device to generate a challenge; @@ -233,7 +238,7 @@ def get(self, request, *args, **kwargs): Start the setup wizard. Redirect if already enabled. """ if default_device(self.request.user): - return redirect(self.redirect_url) + return redirect(self.get_redirect_url()) return super(SetupView, self).get(request, *args, **kwargs) def get_form_list(self): @@ -280,7 +285,7 @@ def done(self, form_list, **kwargs): raise NotImplementedError("Unknown method '%s'" % self.get_method()) django_otp.login(self.request, device) - return redirect(self.redirect_url) + return redirect(self.get_redirect_url()) def get_form_kwargs(self, step=None): kwargs = {} @@ -300,6 +305,9 @@ def get_form_kwargs(self, step=None): }) return kwargs + def get_redirect_url(self): + return self.redirect_url + def get_device(self, **kwargs): """ Uses the data from the setup step and generated key to recreate device. From 02c65704c4791aa1eb96e3076cab5bf389b295df Mon Sep 17 00:00:00 2001 From: Markus Holtermann Date: Thu, 28 Jul 2016 01:39:01 +0200 Subject: [PATCH 3/6] Allowed per-form template overrides --- two_factor/templates/two_factor/_wizard_form_default.html | 3 +++ two_factor/templates/two_factor/_wizard_forms.html | 6 ++---- 2 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 two_factor/templates/two_factor/_wizard_form_default.html diff --git a/two_factor/templates/two_factor/_wizard_form_default.html b/two_factor/templates/two_factor/_wizard_form_default.html new file mode 100644 index 000000000..2a974048c --- /dev/null +++ b/two_factor/templates/two_factor/_wizard_form_default.html @@ -0,0 +1,3 @@ + + {{ wizard.form }} +
diff --git a/two_factor/templates/two_factor/_wizard_forms.html b/two_factor/templates/two_factor/_wizard_forms.html index 82ed22d01..60bf5bffb 100644 --- a/two_factor/templates/two_factor/_wizard_forms.html +++ b/two_factor/templates/two_factor/_wizard_forms.html @@ -1,4 +1,2 @@ - - {{ wizard.management_form }} - {{ wizard.form }} -
+{{ wizard.management_form }} +{% include wizard_form_template|default:"two_factor/_wizard_form_default.html" %} From 407b179069bc39079f13ad38e6857dde9b2a2678 Mon Sep 17 00:00:00 2001 From: Markus Holtermann Date: Thu, 28 Jul 2016 01:40:00 +0200 Subject: [PATCH 4/6] Stopped needlessly calling super --- two_factor/views/utils.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/two_factor/views/utils.py b/two_factor/views/utils.py index d5b986eb9..3145106f9 100644 --- a/two_factor/views/utils.py +++ b/two_factor/views/utils.py @@ -136,7 +136,27 @@ def post(self, *args, **kwargs): self.steps.current) return self.render_goto_step(self.steps.all[-1]) - return super(IdempotentSessionWizardView, self).post(*args, **kwargs) + # -- Duplicated code from upstream + # get the form for the current step + form = self.get_form(data=self.request.POST, files=self.request.FILES) + + # and try to validate + if form.is_valid(): + # if the form is valid, store the cleaned data and files. + self.storage.set_step_data(self.steps.current, + self.process_step(form)) + self.storage.set_step_files(self.steps.current, + self.process_step_files(form)) + + # check if the current step is the last step + if self.steps.current == self.steps.last: + # no more steps, render done view + return self.render_done(form, **kwargs) + else: + # proceed to the next step + return self.render_next_step(form) + return self.render(form) + # -- End duplicated code from upstream def process_step(self, form): """ From cb868496813606e6824add707bd470bbec9d21e2 Mon Sep 17 00:00:00 2001 From: Markus Holtermann Date: Thu, 28 Jul 2016 01:40:40 +0200 Subject: [PATCH 5/6] Added admin themed views --- tests/test_admin.py | 11 +- two_factor/admin.py | 138 +++++++++++++++++- two_factor/templates/admin/base.html | 8 + .../templates/two_factor/admin/_style.html | 56 +++++++ .../two_factor/admin/_wizard_actions.html | 14 ++ .../two_factor/admin/_wizard_form_auth.html | 21 +++ .../two_factor/admin/_wizard_form_backup.html | 8 + .../admin/_wizard_form_generator.html | 8 + .../two_factor/admin/_wizard_form_method.html | 9 ++ .../admin/_wizard_form_phone_number.html | 8 + .../two_factor/admin/_wizard_form_token.html | 8 + .../admin/_wizard_form_validation.html | 8 + .../admin/_wizard_form_yubikey.html | 8 + .../two_factor/admin/backup_tokens.html | 33 +++++ .../templates/two_factor/admin/login.html | 65 +++++++++ .../templates/two_factor/admin/profile.html | 56 +++++++ .../templates/two_factor/admin/setup.html | 58 ++++++++ 17 files changed, 504 insertions(+), 13 deletions(-) create mode 100644 two_factor/templates/admin/base.html create mode 100644 two_factor/templates/two_factor/admin/_style.html create mode 100644 two_factor/templates/two_factor/admin/_wizard_actions.html create mode 100644 two_factor/templates/two_factor/admin/_wizard_form_auth.html create mode 100644 two_factor/templates/two_factor/admin/_wizard_form_backup.html create mode 100644 two_factor/templates/two_factor/admin/_wizard_form_generator.html create mode 100644 two_factor/templates/two_factor/admin/_wizard_form_method.html create mode 100644 two_factor/templates/two_factor/admin/_wizard_form_phone_number.html create mode 100644 two_factor/templates/two_factor/admin/_wizard_form_token.html create mode 100644 two_factor/templates/two_factor/admin/_wizard_form_validation.html create mode 100644 two_factor/templates/two_factor/admin/_wizard_form_yubikey.html create mode 100644 two_factor/templates/two_factor/admin/backup_tokens.html create mode 100644 two_factor/templates/two_factor/admin/login.html create mode 100644 two_factor/templates/two_factor/admin/profile.html create mode 100644 two_factor/templates/two_factor/admin/setup.html diff --git a/tests/test_admin.py b/tests/test_admin.py index 317c5b51e..50912ca8c 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- -from django.conf import settings -from django.shortcuts import resolve_url +from django.core.urlresolvers import reverse from django.test import TestCase from django.test.utils import override_settings @@ -21,13 +20,13 @@ def tearDown(self): def test(self): response = self.client.get('/admin/', follow=True) - redirect_to = '%s?next=/admin/' % resolve_url(settings.LOGIN_URL) + redirect_to = '%s?next=/admin/' % reverse('admin:login') self.assertRedirects(response, redirect_to) @override_settings(LOGIN_URL='two_factor:login') def test_named_url(self): response = self.client.get('/admin/', follow=True) - redirect_to = '%s?next=/admin/' % resolve_url(settings.LOGIN_URL) + redirect_to = '%s?next=/admin/' % reverse('admin:login') self.assertRedirects(response, redirect_to) @@ -54,13 +53,13 @@ def setUp(self): def test_otp_admin_without_otp(self): response = self.client.get('/otp_admin/', follow=True) - redirect_to = '%s?next=/otp_admin/' % resolve_url(settings.LOGIN_URL) + redirect_to = '%s?next=/otp_admin/' % reverse('admin:login') self.assertRedirects(response, redirect_to) @override_settings(LOGIN_URL='two_factor:login') def test_otp_admin_without_otp_named_url(self): response = self.client.get('/otp_admin/', follow=True) - redirect_to = '%s?next=/otp_admin/' % resolve_url(settings.LOGIN_URL) + redirect_to = '%s?next=/otp_admin/' % reverse('admin:login') self.assertRedirects(response, redirect_to) def test_otp_admin_with_otp(self): diff --git a/two_factor/admin.py b/two_factor/admin.py index 60a46f714..35e1d8e30 100644 --- a/two_factor/admin.py +++ b/two_factor/admin.py @@ -1,13 +1,115 @@ +from functools import update_wrapper + from django.conf import settings from django.contrib import admin from django.contrib.admin import AdminSite from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth.views import redirect_to_login +from django.core.urlresolvers import reverse from django.shortcuts import resolve_url from django.utils.http import is_safe_url +from django.utils.translation import ugettext from .models import PhoneDevice from .utils import monkeypatch_method +from .views import BackupTokensView, LoginView, ProfileView, SetupView + + +class AdminLoginView(LoginView): + form_templates = { + 'auth': 'two_factor/admin/_wizard_form_auth.html', + 'token': 'two_factor/admin/_wizard_form_token.html', + 'backup': 'two_factor/admin/_wizard_form_backup.html', + } + redirect_url = 'admin:two_factor:setup' + template_name = 'two_factor/admin/login.html' + + def get_context_data(self, form, **kwargs): + context = super(AdminLoginView, self).get_context_data(form, **kwargs) + if self.kwargs['extra_context']: + context.update(self.kwargs['extra_context']) + user_is_validated = getattr(self.request.user, 'is_verified', None) + context.update({ + 'cancel_url': reverse('admin:index' if user_is_validated else 'admin:login'), + 'wizard_form_template': self.form_templates.get(self.steps.current), + }) + return context + + def get_redirect_url(self): + redirect_to = self.request.GET.get(self.redirect_field_name, '') + url_is_safe = is_safe_url(url=redirect_to, host=self.request.get_host()) + if url_is_safe: + self.request.session[REDIRECT_FIELD_NAME] = redirect_to + user_is_validated = getattr(self.request.user, 'is_verified', None) + if not url_is_safe or not user_is_validated: + redirect_to = resolve_url(self.redirect_url) + return redirect_to + + +admin_login_view = AdminLoginView.as_view() + + +class AdminSetupView(SetupView): + form_templates = { + 'method': 'two_factor/admin/_wizard_form_method.html', + 'generator': 'two_factor/admin/_wizard_form_generator.html', + 'sms': 'two_factor/admin/_wizard_form_phone_number.html', + 'call': 'two_factor/admin/_wizard_form_phone_number.html', + 'validation': 'two_factor/admin/_wizard_form_validation.html', + 'yubikey': 'two_factor/admin/_wizard_form_yubikey.html', + } + redirect_url = 'admin:two_factor:profile' + template_name = 'two_factor/admin/setup.html' + + def get_context_data(self, form, **kwargs): + context = super(AdminSetupView, self).get_context_data(form, **kwargs) + user_is_validated = getattr(self.request.user, 'is_verified', None) + context.update({ + 'cancel_url': reverse('admin:two_factor:profile' if user_is_validated else 'admin:login'), + 'site_header': ugettext("Enable Two-Factor Authentication"), + 'title': ugettext("Enable Two-Factor Authentication"), + 'wizard_form_template': self.form_templates.get(self.steps.current), + }) + return context + + def get_redirect_url(self): + redirect_to = self.request.session.pop(REDIRECT_FIELD_NAME, '') + url_is_safe = is_safe_url(url=redirect_to, host=self.request.get_host()) + user_is_validated = self.request.user.is_verified() + if url_is_safe and user_is_validated: + return redirect_to + return super(AdminSetupView, self).get_redirect_url() + +admin_setup_view = AdminSetupView.as_view() + + +class AdminBackupTokensView(BackupTokensView): + redirect_url = 'admin:two_factor:backup_tokens' + template_name = 'two_factor/admin/backup_tokens.html' + + def get_context_data(self, **kwargs): + context = super(AdminBackupTokensView, self).get_context_data(**kwargs) + context.update({ + 'site_header': ugettext("Backup Tokens"), + 'title': ugettext("Backup Tokens"), + }) + return context + +admin_backup_tokens_view = AdminBackupTokensView.as_view() + + +class AdminProfileView(ProfileView): + template_name = 'two_factor/admin/profile.html' + + def get_context_data(self, **kwargs): + context = super(AdminProfileView, self).get_context_data(**kwargs) + context.update({ + 'site_header': ugettext("Account Security"), + 'title': ugettext("Account Security"), + }) + return context + +admin_profile_view = AdminProfileView.as_view() class AdminSiteOTPRequiredMixin(object): @@ -27,16 +129,38 @@ def has_permission(self, request): return False return request.user.is_verified() + def get_urls(self): + from django.conf.urls import include, url + + def wrap(view, cacheable=False): + def wrapper(*args, **kwargs): + return self.admin_view(view, cacheable)(*args, **kwargs) + wrapper.admin_site = self + return update_wrapper(wrapper, view) + + urlpatterns_2fa = [ + url(r'^profile/$', wrap(self.two_factor_profile), name='profile'), + url(r'^setup/$', self.two_factor_setup, name='setup'), + url(r'^backup/tokens/$', wrap(self.two_factor_backup_tokens), name='backup_tokens'), + ] + + urlpatterns = [ + url(r'^two_factor/', include(urlpatterns_2fa, namespace='two_factor')) + ] + urlpatterns += super(AdminSiteOTPRequiredMixin, self).get_urls() + return urlpatterns + def login(self, request, extra_context=None): - """ - Redirects to the site login page for the given HttpRequest. - """ - redirect_to = request.POST.get(REDIRECT_FIELD_NAME, request.GET.get(REDIRECT_FIELD_NAME)) + return admin_login_view(request, extra_context=extra_context) - if not redirect_to or not is_safe_url(url=redirect_to, host=request.get_host()): - redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL) + def two_factor_profile(self, request): + return admin_profile_view(request) - return redirect_to_login(redirect_to) + def two_factor_setup(self, request): + return admin_setup_view(request) + + def two_factor_backup_tokens(self, request): + return admin_backup_tokens_view(request) class AdminSiteOTPRequired(AdminSiteOTPRequiredMixin, AdminSite): diff --git a/two_factor/templates/admin/base.html b/two_factor/templates/admin/base.html new file mode 100644 index 000000000..2ae0fd2cd --- /dev/null +++ b/two_factor/templates/admin/base.html @@ -0,0 +1,8 @@ +{% extends "admin/base.html" %} + +{% load i18n %} + +{% block userlinks %} +{% trans 'Two Factor' %} / +{{ block.super }} +{% endblock %} diff --git a/two_factor/templates/two_factor/admin/_style.html b/two_factor/templates/two_factor/admin/_style.html new file mode 100644 index 000000000..b7c7cb699 --- /dev/null +++ b/two_factor/templates/two_factor/admin/_style.html @@ -0,0 +1,56 @@ + diff --git a/two_factor/templates/two_factor/admin/_wizard_actions.html b/two_factor/templates/two_factor/admin/_wizard_actions.html new file mode 100644 index 000000000..8eb2674f2 --- /dev/null +++ b/two_factor/templates/two_factor/admin/_wizard_actions.html @@ -0,0 +1,14 @@ +{% load i18n %} + +{# hidden submit button to enable [enter] key #} +
+ +
+ {% trans "Cancel" %} + {% if wizard.steps.prev %} + + {% else %} + + {% endif %} + +
diff --git a/two_factor/templates/two_factor/admin/_wizard_form_auth.html b/two_factor/templates/two_factor/admin/_wizard_form_auth.html new file mode 100644 index 000000000..5187fdc36 --- /dev/null +++ b/two_factor/templates/two_factor/admin/_wizard_form_auth.html @@ -0,0 +1,21 @@ +{% load i18n %} +
+ {% if not form.this_is_the_login_form.errors %}{{ form.username.errors }}{% endif %} + {{ form.username }} +
+
+ {% if not form.this_is_the_login_form.errors %}{{ form.password.errors }}{% endif %} + {{ form.password }} + + +
+{% url 'admin_password_reset' as password_reset_url %} +{% if password_reset_url %} + +{% endif %} + + diff --git a/two_factor/templates/two_factor/admin/_wizard_form_backup.html b/two_factor/templates/two_factor/admin/_wizard_form_backup.html new file mode 100644 index 000000000..eb2a8c855 --- /dev/null +++ b/two_factor/templates/two_factor/admin/_wizard_form_backup.html @@ -0,0 +1,8 @@ +
+ {% if not form.this_is_the_login_form.errors %}{{ form.otp_token.errors }}{% endif %} + {{ form.otp_token }} +
+ + diff --git a/two_factor/templates/two_factor/admin/_wizard_form_generator.html b/two_factor/templates/two_factor/admin/_wizard_form_generator.html new file mode 100644 index 000000000..3af4bda26 --- /dev/null +++ b/two_factor/templates/two_factor/admin/_wizard_form_generator.html @@ -0,0 +1,8 @@ +
+ {% if not form.this_is_the_login_form.errors %}{{ form.token.errors }}{% endif %} + {{ form.token }} +
+ + diff --git a/two_factor/templates/two_factor/admin/_wizard_form_method.html b/two_factor/templates/two_factor/admin/_wizard_form_method.html new file mode 100644 index 000000000..9d07b86be --- /dev/null +++ b/two_factor/templates/two_factor/admin/_wizard_form_method.html @@ -0,0 +1,9 @@ +{% load i18n %} +
+ {% if not form.this_is_the_login_form.errors %}{{ form.method.errors }}{% endif %} + {{ form.method }} +
+ + diff --git a/two_factor/templates/two_factor/admin/_wizard_form_phone_number.html b/two_factor/templates/two_factor/admin/_wizard_form_phone_number.html new file mode 100644 index 000000000..6ea791219 --- /dev/null +++ b/two_factor/templates/two_factor/admin/_wizard_form_phone_number.html @@ -0,0 +1,8 @@ +
+ {% if not form.this_is_the_login_form.errors %}{{ form.number.errors }}{% endif %} + {{ form.number }} +
+ + diff --git a/two_factor/templates/two_factor/admin/_wizard_form_token.html b/two_factor/templates/two_factor/admin/_wizard_form_token.html new file mode 100644 index 000000000..715e1a643 --- /dev/null +++ b/two_factor/templates/two_factor/admin/_wizard_form_token.html @@ -0,0 +1,8 @@ +
+ {% if not form.this_is_the_login_form.errors %}{{ form.otp_token.errors }}{% endif %} + {{ form.otp_token }} +
+ + diff --git a/two_factor/templates/two_factor/admin/_wizard_form_validation.html b/two_factor/templates/two_factor/admin/_wizard_form_validation.html new file mode 100644 index 000000000..1300e4acf --- /dev/null +++ b/two_factor/templates/two_factor/admin/_wizard_form_validation.html @@ -0,0 +1,8 @@ +
+ {% if not form.this_is_the_login_form.errors %}{{ form.token.errors }}{% endif %} + {{ form.token }} +
+ + diff --git a/two_factor/templates/two_factor/admin/_wizard_form_yubikey.html b/two_factor/templates/two_factor/admin/_wizard_form_yubikey.html new file mode 100644 index 000000000..cd3d83c80 --- /dev/null +++ b/two_factor/templates/two_factor/admin/_wizard_form_yubikey.html @@ -0,0 +1,8 @@ +
+ {% if not form.this_is_the_login_form.errors %}{{ form.token.errors }}{% endif %} + {{ form.token }} +
+ + diff --git a/two_factor/templates/two_factor/admin/backup_tokens.html b/two_factor/templates/two_factor/admin/backup_tokens.html new file mode 100644 index 000000000..70a60d77f --- /dev/null +++ b/two_factor/templates/two_factor/admin/backup_tokens.html @@ -0,0 +1,33 @@ +{% extends "admin/login.html" %} + +{% load i18n %} + +{% block content %} +
+
{% csrf_token %} +
+ {% blocktrans %}Backup tokens can be used when your primary and backup phone numbers aren't available. The backup tokens below can be used for login verification. If you've used up all your backup tokens, you can generate a new set of backup tokens. Only the backup tokens shown below will be valid.{% endblocktrans %} +
+ +
+ {% if device.token_set.count %} +
    + {% for token in device.token_set.all %} +
  • {{ token.token }}
  • + {% endfor %} +
+

{% blocktrans %}Print these tokens and keep them somewhere safe.{% endblocktrans %}

+ {% else %} +

{% trans "You don't have any backup codes yet." %}

+ {% endif %} +
+ + + +
+ +
+{% endblock %} diff --git a/two_factor/templates/two_factor/admin/login.html b/two_factor/templates/two_factor/admin/login.html new file mode 100644 index 000000000..6fd804b5b --- /dev/null +++ b/two_factor/templates/two_factor/admin/login.html @@ -0,0 +1,65 @@ +{% extends "admin/login.html" %} + +{% load i18n admin_static two_factor %} + +{% block extrastyle %}{{ block.super }}{% include "two_factor/admin/_style.html" %}{% endblock %} + +{% block content %} +{% if form.errors and not form.non_field_errors and not form.this_is_the_login_form.errors %} +

+{% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %} +

+{% endif %} + +{% if form.non_field_errors or form.this_is_the_login_form.errors %} +{% for error in form.non_field_errors|add:form.this_is_the_login_form.errors %} +

+ {{ error }} +

+{% endfor %} +{% endif %} + +
+
{% csrf_token %} +
+ {% if wizard.steps.current == 'auth' %} + {% blocktrans %}Enter your credentials.{% endblocktrans %} + {% elif wizard.steps.current == 'token' %} + {% if device.method == 'call' %} + {% blocktrans %}We are calling your phone right now, please enter the digits you hear.{% endblocktrans %} + {% elif device.method == 'sms' %} + {% blocktrans %}We sent you a text message, please enter the tokens we sent.{% endblocktrans %} + {% else %} + {% blocktrans %}Please enter the tokens generated by your token generator.{% endblocktrans %} + {% endif %} + {% elif wizard.steps.current == 'backup' %} + {% 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 %} + {% endif %} +
+ + {% include "two_factor/_wizard_forms.html" %} + + {% include "two_factor/admin/_wizard_actions.html" %} + + {% if other_devices %} +
+ {% trans "Or, alternatively, use one of your backup phones:" %}

+
+
+ {% for other in other_devices %} + {{ other|device_action }} + {% endfor %} +
+ {% endif %} + + {% if backup_tokens %} +
+ + +
+ {% endif %} + +
+ +
+{% endblock %} diff --git a/two_factor/templates/two_factor/admin/profile.html b/two_factor/templates/two_factor/admin/profile.html new file mode 100644 index 000000000..f98086bd8 --- /dev/null +++ b/two_factor/templates/two_factor/admin/profile.html @@ -0,0 +1,56 @@ +{% extends "admin/login.html" %} + +{% load i18n two_factor %} + +{% block content %} +
+
+ {% if default_device %} + {% if default_device_type == 'TOTPDevice' %} + {% blocktrans %}Tokens will be generated by your token generator.{% endblocktrans %} + {% elif default_device_type == 'PhoneDevice' %} + {% blocktrans with primary=default_device|device_action %}Primary method: {{ primary }}{% endblocktrans %} + {% elif default_device_type == 'RemoteYubikeyDevice' %} + {% blocktrans %}Tokens will be generated by your YubiKey.{% endblocktrans %} + {% endif %} + {% else %} +

{% blocktrans %}Two-factor authentication is not enabled for your account. Enable two-factor authentication for enhanced account security.{% endblocktrans %}

+

{% trans "Enable Two-Factor Authentication" %}

+ {% endif %} +
+ +
+

{% trans "Backup Phone Numbers" %}

+ TODO + {% comment %} +

{% blocktrans %}If your primary method is not available, we are able to send backup tokens to the phone numbers listed below.{% endblocktrans %}

+
    + {% for phone in backup_phones %} +
  • + {{ phone|device_action }} +
    + {% csrf_token %} + +
    +
  • + {% endfor %} +
+

{% trans "Add Phone Number" %}

+ {% endcomment %} +
+ +
+

{% trans "Backup Tokens" %}

+

+ {% blocktrans %}If you don't have any device with you, you can access your account using backup tokens.{% endblocktrans %} + {% blocktrans count counter=backup_tokens %}You have only one backup token remaining.{% plural %}You have {{ counter }} backup tokens remaining.{% endblocktrans %} +

+

{% trans "Show Codes" %}

+
+ + + +
+{% endblock %} diff --git a/two_factor/templates/two_factor/admin/setup.html b/two_factor/templates/two_factor/admin/setup.html new file mode 100644 index 000000000..97c771ac2 --- /dev/null +++ b/two_factor/templates/two_factor/admin/setup.html @@ -0,0 +1,58 @@ +{% extends "admin/login.html" %} + +{% load i18n admin_static two_factor %} + +{% block extrastyle %}{{ block.super }}{% include "two_factor/admin/_style.html" %}{% endblock %} + +{% block content %} +{% if form.errors and not form.non_field_errors and not form.this_is_the_login_form.errors %} +

+{% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %} +

+{% endif %} + +{% if form.non_field_errors or form.this_is_the_login_form.errors %} +{% for error in form.non_field_errors|add:form.this_is_the_login_form.errors %} +

+ {{ error }} +

+{% endfor %} +{% endif %} + +
+
{% csrf_token %} +
+ {% if wizard.steps.current == 'welcome' %} + {% blocktrans %}You are about to take your account security to the next level. Follow the steps in this wizard to enable two-factor authentication.{% endblocktrans %} + {% elif wizard.steps.current == 'method' %} + {% blocktrans %}Please select which authentication method you would like to use.{% endblocktrans %} + {% elif wizard.steps.current == 'generator' %} + {% blocktrans %}To start using a token generator, please use your smartphone to scan the QR code below. For example, use Google Authenticator. Then, enter the token generated by the app. {% endblocktrans %} + QR Code + {% elif wizard.steps.current == 'sms' %} + {% blocktrans %}Please enter the phone number you wish to receive the text messages on. This number will be validated in the next step. {% endblocktrans %} + {% elif wizard.steps.current == 'call' %} + {% blocktrans %}Please enter the phone number you wish to be called on. This number will be validated in the next step. {% endblocktrans %} + {% elif wizard.steps.current == 'validation' %} + {% if challenge_succeeded %} + {% if device.method == 'call' %} + {% blocktrans %}We are calling your phone right now, please enter the digits you hear.{% endblocktrans %} + {% elif device.method == 'sms' %} + {% blocktrans %}We sent you a text message, please enter the tokens we sent.{% endblocktrans %} + {% endif %} + {% else %} +
+ + {% include "two_factor/_wizard_forms.html" %} + + {% include "two_factor/admin/_wizard_actions.html" %} + +
+ +
+{% endblock %} From 714e6cbe024362ea3dc411624e5289eda006d5fc Mon Sep 17 00:00:00 2001 From: Markus Holtermann Date: Thu, 28 Jul 2016 01:55:23 +0200 Subject: [PATCH 6/6] Reworked admin auto-patching --- docs/configuration.rst | 3 +++ two_factor/admin.py | 41 +++++++++++++++++++++++------------------ two_factor/utils.py | 8 -------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index 825ab3c58..45d8c522f 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -11,6 +11,9 @@ General Settings The admin currently does not enforce one-time passwords being set for admin users. +``TWO_FACTOR_FORCE_OTP_ADMIN`` (default: ``False``) + Whether the Django admin will enforce 2 factor authentication. + ``TWO_FACTOR_CALL_GATEWAY`` (default: ``None``) Which gateway to use for making phone calls. Should be set to a module or object providing a ``make_call`` method. Currently two gateways are bundled: diff --git a/two_factor/admin.py b/two_factor/admin.py index 35e1d8e30..54c7ce289 100644 --- a/two_factor/admin.py +++ b/two_factor/admin.py @@ -4,14 +4,12 @@ from django.contrib import admin from django.contrib.admin import AdminSite from django.contrib.auth import REDIRECT_FIELD_NAME -from django.contrib.auth.views import redirect_to_login from django.core.urlresolvers import reverse from django.shortcuts import resolve_url from django.utils.http import is_safe_url from django.utils.translation import ugettext from .models import PhoneDevice -from .utils import monkeypatch_method from .views import BackupTokensView, LoginView, ProfileView, SetupView @@ -129,6 +127,9 @@ def has_permission(self, request): return False return request.user.is_verified() + +class AdminSiteOTPMixin(object): + def get_urls(self): from django.conf.urls import include, url @@ -147,7 +148,7 @@ def wrapper(*args, **kwargs): urlpatterns = [ url(r'^two_factor/', include(urlpatterns_2fa, namespace='two_factor')) ] - urlpatterns += super(AdminSiteOTPRequiredMixin, self).get_urls() + urlpatterns += super(AdminSiteOTPMixin, self).get_urls() return urlpatterns def login(self, request, extra_context=None): @@ -163,32 +164,36 @@ def two_factor_backup_tokens(self, request): return admin_backup_tokens_view(request) -class AdminSiteOTPRequired(AdminSiteOTPRequiredMixin, AdminSite): +class AdminSiteOTP(AdminSiteOTPMixin, AdminSite): """ - AdminSite enforcing OTP verified staff users. + AdminSite using OTP login. """ pass -def patch_admin(): - @monkeypatch_method(AdminSite) - def login(self, request, extra_context=None): - """ - Redirects to the site login page for the given HttpRequest. - """ - redirect_to = request.POST.get(REDIRECT_FIELD_NAME, request.GET.get(REDIRECT_FIELD_NAME)) +class AdminSiteOTPRequired(AdminSiteOTPMixin, AdminSiteOTPRequiredMixin, AdminSite): + """ + AdminSite enforcing OTP verified staff users. + """ + pass - if not redirect_to or not is_safe_url(url=redirect_to, host=request.get_host()): - redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL) - return redirect_to_login(redirect_to) +__default_admin_site__ = None -def unpatch_admin(): - setattr(AdminSite, 'login', original_login) +def patch_admin(): + global __default_admin_site__ + __default_admin_site__ = admin.site.__class__ + if getattr(settings, 'TWO_FACTOR_FORCE_OTP_ADMIN', False): + admin.site.__class__ = AdminSiteOTPRequired + else: + admin.site.__class__ = AdminSiteOTP -original_login = AdminSite.login +def unpatch_admin(): + global __default_admin_site__ + admin.site.__class__ = __default_admin_site__ + __default_admin_site__ = None class PhoneDeviceAdmin(admin.ModelAdmin): diff --git a/two_factor/utils.py b/two_factor/utils.py index aad926c01..8d8660408 100644 --- a/two_factor/utils.py +++ b/two_factor/utils.py @@ -47,14 +47,6 @@ def get_otpauth_url(accountname, secret, issuer=None, digits=None): return 'otpauth://totp/%s?%s' % (label, urlencode(query)) -# from http://mail.python.org/pipermail/python-dev/2008-January/076194.html -def monkeypatch_method(cls): - def decorator(func): - setattr(cls, func.__name__, func) - return func - return decorator - - def totp_digits(): """ Returns the number of digits (as configured by the TWO_FACTOR_TOTP_DIGITS setting)