From 81efb631e2a771520f8321d46aa388fb2fe12ddc Mon Sep 17 00:00:00 2001
From: Lindsay {% blocktrans trimmed %}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 %} {% blocktrans %}Print these tokens and keep them somewhere safe.{% endblocktrans %} {% trans "You don't have any backup codes yet." %} {% blocktrans %}Enter your credentials.{% endblocktrans %} {% blocktrans trimmed %}We are calling your phone right now, please enter the
+ digits you hear.{% endblocktrans %} {% blocktrans trimmed %}We sent you a text message, please enter the tokens we
+ sent.{% endblocktrans %} {% blocktrans trimmed %}Please enter the tokens generated by your token
+ generator.{% endblocktrans %} {% blocktrans trimmed %}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 %} {% blocktrans trimmed %}The page you requested, enforces users to verify using
+ two-factor authentication for security reasons. You need to enable these
+ security features in order to access this page.{% endblocktrans %} {% blocktrans trimmed %}Two-factor authentication is not enabled for your
+ account. Enable two-factor authentication for enhanced account
+ security.{% endblocktrans %}
+ {% trans "Go back" %}
+
+ {% trans "Enable Two-Factor Authentication" %}
+ {% blocktrans trimmed %}You'll be adding a backup phone number to your
+ account. This number will be used if your primary method of
+ registration is not available.{% endblocktrans %} {% blocktrans trimmed %}We've sent a token to your phone number. Please
+ enter the token you've received.{% endblocktrans %} {% blocktrans trimmed %}You are about to take your account security to the
+ next level. Follow the steps in this wizard to enable two-factor
+ authentication.{% endblocktrans %} {% blocktrans trimmed %}Please select which authentication method you would
+ like to use.{% endblocktrans %} {% blocktrans trimmed %}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 %} {% blocktrans trimmed %}Please enter the phone number you wish to receive the
+ text messages on. This number will be validated in the next step.
+ {% endblocktrans %} {% blocktrans trimmed %}Please enter the phone number you wish to be called on.
+ This number will be validated in the next step. {% endblocktrans %} {% blocktrans trimmed %}We are calling your phone right now, please enter the
+ digits you hear.{% endblocktrans %} {% blocktrans trimmed %}We sent you a text message, please enter the tokens we
+ sent.{% endblocktrans %} {% blocktrans trimmed %}We've
+ encountered an issue with the selected authentication method. Please
+ go back and verify that you entered your information correctly, try
+ again, or use a different authentication method instead. If the issue
+ persists, contact the site administrator.{% endblocktrans %} {% blocktrans trimmed %}To identify and verify your YubiKey, please insert a
+ token in the field below. Your YubiKey will be linked to your
+ account.{% endblocktrans %} {% blocktrans trimmed %}Congratulations, you've successfully enabled two-factor
+ authentication.{% endblocktrans %} {% blocktrans trimmed %}However, it might happen that you don't have access to
+ your primary token device. To enable account recovery, add a phone
+ number.{% endblocktrans %} {% trans "Add Phone Number" %} {% blocktrans trimmed %}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 %} {% blocktrans trimmed %}You are about to change your double authentication method form
+ Google authenticator to phone method.{% endblocktrans %} {% blocktrans trimmed %}We've sent a token to your phone number. Please
+ enter the token you've received.{% endblocktrans %} {% blocktrans trimmed %}You are about to disable two-factor authentication. This
+ weakens your account security, are you sure?{% endblocktrans %} {% trans "Tokens will be generated by your token generator." %} {% blocktrans with primary=default_device|device_action %}Primary method: {{ primary }}{% endblocktrans %} {% blocktrans %}Tokens will be generated by your YubiKey.{% endblocktrans %} {% blocktrans trimmed %}If your primary method is not available, we are able to
+ send backup tokens to the phone numbers listed below.{% endblocktrans %} {% trans "Add Phone Number" %}
+ {% blocktrans trimmed %}If you don't have any device with you, you can access
+ your account using backup tokens.{% endblocktrans %}
+ {% blocktrans trimmed count counter=backup_tokens %}
+ You have only one backup token remaining.
+ {% plural %}
+ You have {{ counter }} backup tokens remaining.
+ {% endblocktrans %}
+ {% blocktrans trimmed %}However we strongly discourage you to do so, you can
+ also disable two-factor authentication for your account.{% endblocktrans %}
+ {% trans "Disable Two-Factor Authentication" %} {% blocktrans trimmed %}
+ You choose to get the 6-digits authentication code using Google Authenticator.
+ If you want to receive the code by SMS, please click below and follow the instructions.{% endblocktrans %}
+
+ {% trans "Change Two-Factor Authentication method" %}
+ {% blocktrans trimmed %}
+ You choose to get the 6-digits authentication code using SMS.
+ If you want to use Google Authenticator instead, please click below and follow the instructions.{% endblocktrans %}
+
+ {% trans "Change Two-Factor Authentication method" %} {% blocktrans trimmed %}Two-factor authentication is not enabled for your
+ account. Enable two-factor authentication for enhanced account
+ security.{% endblocktrans %}
+ {% trans "Enable Two-Factor Authentication" %}
+ {% blocktrans trimmed %}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 %} {% blocktrans trimmed %}You are about to change your double authentication method form
+ Google authenticator to phone method.{% endblocktrans %} {% blocktrans trimmed %}We've sent a token to your phone number. Please
+ enter the token you've received.{% endblocktrans %}{% block title %}{% trans "Backup Tokens" %}{% endblock %}
+
+ {% for token in device.token_set.all %}
+
+ {% block title %}{% trans "Login" %}{% endblock %}
+
+ {% if wizard.steps.current == 'auth' %}
+ {% block title %}{% trans "Permission Denied" %}{% endblock %}
+
+ {% block title %}{% trans "Add Backup Phone" %}{% endblock %}
+
+ {% if wizard.steps.current == 'setup' %}
+ {% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}
+ {% if wizard.steps.current == 'welcome' %}
+ {% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}
+
+ {% block title %}{% trans "Change Two-Factor Authentication method" %}{% endblock %}
+ {% block title %}{% trans "Change Two-Factor Authentication method" %}{% endblock %}
+
+ {% if wizard.steps.current == 'setup' %}
+ {% block title %}{% trans "Disable Two-factor Authentication" %}{% endblock %}
+ {% block title %}{% trans "Account Security" %}{% endblock %}
+ {% if default_device %}
+ {% if default_device_type == 'TOTPDevice' %}
+ {% trans "Backup Phone Numbers" %}
+
+ {% for phone in backup_phones %}
+
+ {% trans "Backup Tokens" %}
+ {% trans "Disable Two-Factor Authentication" %}
+ {% trans "Change Two-Factor Authentication method" %}
+
+ {% if default_device_type == 'TOTPDevice' %}
+ {% block title %}{% trans "Change Two-Factor Authentication method" %}{% endblock %}
+ {% block title %}{% trans "Change Two-Factor Authentication method" %}{% endblock %}
+
+ {% if wizard.steps.current == 'setup' %}
+ {% trans "Disable Two-Factor Authentication" %}
also disable two-factor authentication for your account.{% endblocktrans %}
{% trans "Disable Two-Factor Authentication" %}
+ + +{% blocktrans trimmed %} + You choose to get the 6-digits authentication code using Google Authenticator. + If you want to receive the code by SMS, please click below and follow the instructions.{% endblocktrans %} +
++ {% trans "Change Two-Factor Authentication method" %}
+ {% elif default_device_type == 'PhoneDevice' %} ++ {% blocktrans trimmed %} + You choose to get the 6-digits authentication code using SMS. + If you want to use Google Authenticator instead, please click below and follow the instructions.{% endblocktrans %} +
++ {% trans "Change Two-Factor Authentication method" %}
+ {% endif %} + + {% else %}{% blocktrans trimmed %}Two-factor authentication is not enabled for your account. Enable two-factor authentication for enhanced account diff --git a/two_factor/urls.py b/two_factor/urls.py index 8482978dd..72937a113 100644 --- a/two_factor/urls.py +++ b/two_factor/urls.py @@ -2,7 +2,8 @@ from two_factor.views import ( BackupTokensView, DisableView, LoginView, PhoneDeleteView, PhoneSetupView, - ProfileView, QRGeneratorView, SetupCompleteView, SetupView, + ProfileView, QRGeneratorView, SetupCompleteView, SetupView, ResetSetupGeneratorView, + ResetSetupPhoneView, ) core = [ @@ -26,6 +27,16 @@ SetupCompleteView.as_view(), name='setup_complete', ), + path( + 'account/two_factor/setup/reset/generator/', + view=ResetSetupGeneratorView.as_view(), + name='setup_reset_generator', + ), + path( + 'account/two_factor/setup/reset/phone/', + view=ResetSetupPhoneView.as_view(), + name='setup_reset_phone', + ), path( 'account/two_factor/backup/tokens/', BackupTokensView.as_view(), @@ -55,5 +66,4 @@ name='disable', ), ] - urlpatterns = (core + profile, 'two_factor') diff --git a/two_factor/views/__init__.py b/two_factor/views/__init__.py index c2abab1e4..7c0cf38f0 100644 --- a/two_factor/views/__init__.py +++ b/two_factor/views/__init__.py @@ -1,6 +1,7 @@ from .core import ( BackupTokensView, LoginView, PhoneDeleteView, PhoneSetupView, - QRGeneratorView, SetupCompleteView, SetupView, + QRGeneratorView, ResetSetupGeneratorView, ResetSetupPhoneView, SetupCompleteView, SetupView ) from .mixins import OTPRequiredMixin from .profile import DisableView, ProfileView + diff --git a/two_factor/views/core.py b/two_factor/views/core.py index c7ce0defc..c4813a3dc 100644 --- a/two_factor/views/core.py +++ b/two_factor/views/core.py @@ -33,10 +33,10 @@ from django_otp.decorators import otp_required from django_otp.plugins.otp_static.models import StaticDevice, StaticToken + from two_factor import signals from two_factor.models import get_available_methods, random_hex_str from two_factor.utils import totp_digits - from ..forms import ( AuthenticationTokenForm, BackupTokenForm, DeviceValidationForm, MethodForm, PhoneNumberForm, PhoneNumberMethodForm, TOTPDeviceForm, YubiKeyDeviceForm, @@ -434,6 +434,7 @@ def get_form_list(self): """ Check if there is only one method, then skip the MethodForm from form_list """ + form_list = super().get_form_list() available_methods = get_available_methods() if len(available_methods) == 1: @@ -465,7 +466,6 @@ def done(self, form_list, **kwargs): del self.request.session[self.session_key_name] except KeyError: pass - # TOTPDeviceForm if self.get_method() == 'generator': form = [form for form in form_list if isinstance(form, TOTPDeviceForm)][0] @@ -530,9 +530,12 @@ def get_device(self, **kwargs): def get_key(self, step): self.storage.extra_data.setdefault('keys', {}) + if step in self.storage.extra_data['keys']: return self.storage.extra_data['keys'].get(step) + key = random_hex_str(20) + self.storage.extra_data['keys'][step] = key return key @@ -549,6 +552,7 @@ def get_context_data(self, form, **kwargs): elif self.steps.current == 'validation': context['device'] = self.get_device() context['cancel_url'] = resolve_url(settings.LOGIN_REDIRECT_URL) + return context def process_step(self, form): @@ -664,6 +668,7 @@ def get_key(self): if self.key_name not in self.storage.extra_data: key = random_hex_str(20) self.storage.extra_data[self.key_name] = key + return self.storage.extra_data[self.key_name] def get_context_data(self, form, **kwargs): @@ -700,6 +705,99 @@ def get_context_data(self): } +@class_view_decorator(never_cache) +@class_view_decorator(login_required) +class ResetSetupGeneratorView(IdempotentSessionWizardView): + """ + View for changing the two-factor authentication method from phone number to token generator. + """ + template_name = 'two_factor/core/setup_reset_generator.html' + success_url = settings.LOGIN_REDIRECT_URL + qrcode_url = 'two_factor:qr' + session_key_name = 'django_two_factor-qr_secret_key' + + form_list = ( + ('generator', TOTPDeviceForm), + ) + + def done(self, form_list, **kwargs): + """ + Finish the wizard. Save all forms and redirect. + """ + # Delete previous device + self.delete_previous_device() + + # Remove secret key used for QR code generation + try: + del self.request.session[self.session_key_name] + except KeyError: + pass + + form = [form for form in form_list if isinstance(form, TOTPDeviceForm)][0] + device = form.save() + django_otp.login(self.request, device) + return redirect(self.success_url) + + def get_form_kwargs(self, step=None): + kwargs = {} + kwargs.update({ + 'key': self.get_key(step), + 'user': self.request.user, + }) + return kwargs + + def get_key(self, step): + self.storage.extra_data.setdefault('keys', {}) + if step in self.storage.extra_data['keys']: + return self.storage.extra_data['keys'].get(step) + key = random_hex_str(20) + self.storage.extra_data['keys'][step] = key + return key + + def get_context_data(self, form, **kwargs): + context = super().get_context_data(form, **kwargs) + key = self.get_key('generator') + rawkey = unhexlify(key.encode('ascii')) + b32key = b32encode(rawkey).decode('utf-8') + self.request.session[self.session_key_name] = b32key + context.update({ + 'QR_URL': reverse(self.qrcode_url) + }) + context['cancel_url'] = resolve_url(settings.LOGIN_REDIRECT_URL) + + return context + + +@class_view_decorator(never_cache) +@class_view_decorator(login_required) +class ResetSetupPhoneView(PhoneSetupView): + """ + View for changing the two-factor authentication method from token generator to phone number. + """ + template_name = 'two_factor/core/setup_reset_phone.html' + + def get_device(self, **kwargs): + """ + Uses the data from the setup step and generated key to recreate device. + """ + kwargs = kwargs or {} + kwargs['name'] = 'default' + kwargs['user'] = self.request.user + kwargs.update(self.storage.validated_step_data.get('setup', {})) + return PhoneDevice(key=self.get_key(), **kwargs) + + def done(self, form_list, **kwargs): + """ + Finish the wizard. Save all forms and redirect. + """ + self.delete_previous_device() + # Create new device for user + device = self.get_device(user=self.request.user) + device.save() + django_otp.login(self.request, device) + return redirect(self.success_url) + + @class_view_decorator(never_cache) @class_view_decorator(login_required) class QRGeneratorView(View): diff --git a/two_factor/views/utils.py b/two_factor/views/utils.py index ad3b92bfa..ad8462e29 100644 --- a/two_factor/views/utils.py +++ b/two_factor/views/utils.py @@ -8,6 +8,7 @@ from django.contrib.auth import load_backend from django.core.exceptions import SuspiciousOperation from django.core.signing import BadSignature, SignatureExpired +from django_otp import devices_for_user, user_has_device from django.utils import baseconv from django.utils.decorators import method_decorator from django.utils.encoding import force_bytes @@ -81,6 +82,13 @@ class IdempotentSessionWizardView(SessionWizardView): storage_name = 'two_factor.views.utils.ExtraSessionStorage' idempotent_dict = {} + def delete_previous_device(self): + # Delete the previous device associated to the user if you want to change device + if user_has_device(self.request.user): + devices = devices_for_user(self.request.user) + for current_device in devices: + current_device.delete() + def is_step_visible(self, step): """ Returns whether the given `step` should be included in the wizard; it