From c24cedf772d2cf558a40a679939bd3a7addca27d 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 %}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 %}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 %}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 %}Please select which authentication method you would
+ like to use.{% 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 %}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 change this and use another method, please click on the link below.{% endblocktrans %}
+
+ {% trans "Change Two-Factor Authentication method" %}
+ {% blocktrans trimmed %}
+ You choose to get the 6-digits authentication code using SMS.
+ If you want to change this and use another method, please click on the link below.{% endblocktrans %}
+
+ {% trans "Change Two-Factor Authentication method" %}
+ {% blocktrans trimmed %}
+ You choose to get the tokens through your YubiKey.
+ If you want to change this and use another method, please click on the link below.{% 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 %}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 %}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 %}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 %}Please select which authentication method you would
+ like to use.{% 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 %}{% 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 %}
+ {% if wizard.steps.current == 'method' %}
+ {% block title %}{% trans "Change Two-Factor Authentication method" %}{% endblock %}
+
+ {% if wizard.steps.current == 'method' %}
+ {% block title %}{% trans "Change Two-Factor Authentication method" %}{% endblock %}
+ {% if wizard.steps.current == 'method' %}
+ {% 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 %}
+ {% if wizard.steps.current == 'method' %}
+ {% block title %}{% trans "Change Two-Factor Authentication method" %}{% endblock %}
+
+ {% if wizard.steps.current == 'method' %}
+ {% block title %}{% trans "Change Two-Factor Authentication method" %}{% endblock %}
+ {% if wizard.steps.current == 'method' %}
+ {% 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 change this and use another method, please click on the link below.{% 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 change this and use another method, please click on the link below.{% endblocktrans %} +
++ {% trans "Change Two-Factor Authentication method" %}
+ {% elif default_device_type == 'YubikeyDevice' %} ++ {% blocktrans trimmed %} + You choose to get the tokens through your YubiKey. + If you want to change this and use another method, please click on the link below.{% 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..0135a5222 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, ResetSetupGeneratorOrYubikeyView, ResetSetupPhoneOrGeneratorView, + ResetSetupPhoneOrYubikeyView, SetupCompleteView, SetupView, ) core = [ @@ -26,6 +27,21 @@ SetupCompleteView.as_view(), name='setup_complete', ), + path( + 'account/two_factor/setup/reset/1/', + view=ResetSetupGeneratorOrYubikeyView.as_view(), + name='setup_reset_generator_or_yubikey', + ), + path( + 'account/two_factor/setup/reset/2/', + view=ResetSetupPhoneOrYubikeyView.as_view(), + name='setup_reset_phone_or_yubikey', + ), + path( + 'account/two_factor/setup/reset/3/', + view=ResetSetupPhoneOrGeneratorView.as_view(), + name='setup_reset_phone_or_generator', + ), path( 'account/two_factor/backup/tokens/', BackupTokensView.as_view(), @@ -55,5 +71,4 @@ name='disable', ), ] - urlpatterns = (core + profile, 'two_factor') diff --git a/two_factor/views/__init__.py b/two_factor/views/__init__.py index c2abab1e4..c58a185d7 100644 --- a/two_factor/views/__init__.py +++ b/two_factor/views/__init__.py @@ -1,6 +1,8 @@ from .core import ( BackupTokensView, LoginView, PhoneDeleteView, PhoneSetupView, - QRGeneratorView, SetupCompleteView, SetupView, + QRGeneratorView, ResetSetupGeneratorOrYubikeyView, ResetSetupPhoneOrGeneratorView, + ResetSetupPhoneOrYubikeyView, 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..75ef0a178 100644 --- a/two_factor/views/core.py +++ b/two_factor/views/core.py @@ -39,14 +39,14 @@ from ..forms import ( AuthenticationTokenForm, BackupTokenForm, DeviceValidationForm, MethodForm, - PhoneNumberForm, PhoneNumberMethodForm, TOTPDeviceForm, YubiKeyDeviceForm, + PhoneNumberForm, PhoneNumberMethodForm, ResetGeneratorOrYubikeyMethodForm, ResetPhoneOrGeneratorMethodForm, + ResetPhoneOrYubikeyMethodForm, TOTPDeviceForm, YubiKeyDeviceForm ) from ..models import PhoneDevice, get_available_phone_methods from ..utils import backup_phones, default_device, get_otpauth_url -from .utils import ( +from .utils import (CustomSessionWizardView, IdempotentSessionWizardView, class_view_decorator, - get_remember_device_cookie, validate_remember_device_cookie, -) + get_remember_device_cookie, validate_remember_device_cookie) try: from otp_yubikey.models import ValidationService, RemoteYubikeyDevice @@ -381,7 +381,7 @@ def dispatch(self, request, *args, **kwargs): @class_view_decorator(never_cache) @class_view_decorator(login_required) -class SetupView(IdempotentSessionWizardView): +class SetupView(IdempotentSessionWizardView, CustomSessionWizardView): """ View for handling OTP setup using a wizard. @@ -418,10 +418,6 @@ class SetupView(IdempotentSessionWizardView): 'yubikey': False, } - def get_method(self): - method_data = self.storage.validated_step_data.get('method', {}) - return method_data.get('method', None) - def get(self, request, *args, **kwargs): """ Start the setup wizard. Redirect if already enabled. @@ -442,20 +438,6 @@ def get_form_list(self): self.storage.validated_step_data['method'] = {'method': method_key} return form_list - def render_next_step(self, form, **kwargs): - """ - In the validation step, ask the device to generate a challenge. - """ - next_step = self.steps.next - if next_step == 'validation': - try: - self.get_device().generate_challenge() - kwargs["challenge_succeeded"] = True - except Exception: - logger.exception("Could not generate challenge") - kwargs["challenge_succeeded"] = False - return super().render_next_step(form, **kwargs) - def done(self, form_list, **kwargs): """ Finish the wizard. Save all forms and redirect. @@ -465,7 +447,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] @@ -528,39 +509,12 @@ def get_device(self, **kwargs): raise KeyError("Multiple ValidationService found with name 'default'") return RemoteYubikeyDevice(**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) - if self.steps.current == 'generator': - 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) - }) - 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): if hasattr(form, 'metadata'): self.storage.extra_data.setdefault('forms', {}) self.storage.extra_data['forms'][self.steps.current] = form.metadata return super().process_step(form) - def get_form_metadata(self, step): - self.storage.extra_data.setdefault('forms', {}) - return self.storage.extra_data['forms'].get(step, None) - @class_view_decorator(never_cache) @class_view_decorator(otp_required) @@ -700,6 +654,270 @@ def get_context_data(self): } +@class_view_decorator(never_cache) +@class_view_decorator(login_required) +class ResetSetupGeneratorOrYubikeyView(IdempotentSessionWizardView, CustomSessionWizardView): + """ + View for changing the two-factor authentication method from phone number to token generator or yubikey. + """ + template_name = 'two_factor/core/setup_reset_generator_or_yubikey.html' + success_url = settings.LOGIN_REDIRECT_URL + qrcode_url = 'two_factor:qr' + session_key_name = 'django_two_factor-qr_secret_key' + + form_list = ( + ('method', ResetGeneratorOrYubikeyMethodForm), + ('generator', TOTPDeviceForm), + ('yubikey', YubiKeyDeviceForm) + ) + + condition_dict = { + 'generator': lambda self: self.get_method() == 'generator', + 'yubikey': lambda self: self.get_method() == 'yubikey', + } + idempotent_dict = { + 'yubikey': False, + } + + def done(self, form_list, **kwargs): + """ + Finish the wizard. Save all forms and redirect. + """ + # Remove secret key used for QR code generation + self.delete_previous_device() + + 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 = {} + if step == 'generator': + kwargs.update({ + 'key': self.get_key(step), + 'user': self.request.user, + }) + if step == 'yubikey': + kwargs.update({ + 'device': self.get_device() + }) + metadata = self.get_form_metadata(step) + if metadata: + kwargs.update({ + 'metadata': metadata, + }) + return kwargs + + def get_device(self, **kwargs): + """ + Uses the data from the setup step and generated key to recreate device. + + Only used for call / sms -- generator uses other procedure. + """ + method = self.get_method() + kwargs = kwargs or {} + kwargs['name'] = 'default' + kwargs['user'] = self.request.user + + if method == 'yubikey': + kwargs['public_id'] = self.storage.validated_step_data\ + .get('yubikey', {}).get('token', '')[:-32] + try: + kwargs['service'] = ValidationService.objects.get(name='default') + except ValidationService.DoesNotExist: + raise KeyError("No ValidationService found with name 'default'") + except ValidationService.MultipleObjectsReturned: + raise KeyError("Multiple ValidationService found with name 'default'") + return RemoteYubikeyDevice(**kwargs) + + def get_context_data(self, form, **kwargs): + context = super().get_context_data(form, **kwargs) + if self.steps.current == 'generator': + 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 ResetSetupPhoneOrYubikeyView(IdempotentSessionWizardView, CustomSessionWizardView): + """ + View for changing the two-factor authentication method from token generator to phone number or yubikey. + """ + template_name = 'two_factor/core/setup_reset_phone_or_yubikey.html' + success_url = settings.LOGIN_REDIRECT_URL + + form_list = ( + ('method', ResetPhoneOrYubikeyMethodForm), + ('sms', PhoneNumberForm), + ('call', PhoneNumberForm), + ('validation', DeviceValidationForm), + ('yubikey', YubiKeyDeviceForm), + ) + condition_dict = { + 'call': lambda self: self.get_method() == 'call', + 'sms': lambda self: self.get_method() == 'sms', + 'validation': lambda self: self.get_method() in ('sms', 'call'), + 'yubikey': lambda self: self.get_method() == 'yubikey', + } + idempotent_dict = { + 'yubikey': False, + } + key_name = 'key' + + def done(self, form_list, **kwargs): + """ + Finish the wizard. Save all forms and redirect. + """ + self.delete_previous_device() + + device = self.get_device() + device.save() + + django_otp.login(self.request, device) + return redirect(self.success_url) + + def get_form_kwargs(self, step=None): + kwargs = {} + if step in ('validation', 'yubikey'): + kwargs.update({ + 'device': self.get_device() + }) + metadata = self.get_form_metadata(step) + if metadata: + kwargs.update({ + 'metadata': metadata, + }) + return kwargs + + def get_device(self, **kwargs): + """ + Uses the data from the setup step and generated key to recreate device. + + Only used for call / sms -- generator uses other procedure. + """ + method = self.get_method() + kwargs = kwargs or {} + kwargs['name'] = 'default' + kwargs['user'] = self.request.user + + if method in ('call', 'sms'): + kwargs['method'] = method + kwargs['number'] = self.storage.validated_step_data\ + .get(method, {}).get('number') + return PhoneDevice(key=self.get_key(method), **kwargs) + + if method == 'yubikey': + kwargs['public_id'] = self.storage.validated_step_data\ + .get('yubikey', {}).get('token', '')[:-32] + try: + kwargs['service'] = ValidationService.objects.get(name='default') + except ValidationService.DoesNotExist: + raise KeyError("No ValidationService found with name 'default'") + except ValidationService.MultipleObjectsReturned: + raise KeyError("Multiple ValidationService found with name 'default'") + return RemoteYubikeyDevice(**kwargs) + + def get_context_data(self, form, **kwargs): + context = super().get_context_data(form, **kwargs) + if self.steps.current == 'validation': + context['device'] = self.get_device() + context['cancel_url'] = resolve_url(settings.LOGIN_REDIRECT_URL) + return context + + +class ResetSetupPhoneOrGeneratorView(IdempotentSessionWizardView, CustomSessionWizardView): + """ + View for changing the two-factor authentication method from yubikey to phone number or phone. + """ + + success_url = settings.LOGIN_REDIRECT_URL + + template_name = 'two_factor/core/setup_reset_phone_or_generator.html' + form_list = ( + ('method', ResetPhoneOrGeneratorMethodForm), + ('generator', TOTPDeviceForm), + ('sms', PhoneNumberForm), + ('call', PhoneNumberForm), + ('validation', DeviceValidationForm), + ) + condition_dict = { + 'generator': lambda self: self.get_method() == 'generator', + 'call': lambda self: self.get_method() == 'call', + 'sms': lambda self: self.get_method() == 'sms', + 'validation': lambda self: self.get_method() in ('sms', 'call'), + } + + def done(self, form_list, **kwargs): + """ + Finish the wizard. Save all forms and redirect. + """ + self.delete_previous_device() + # Remove secret key used for QR code generation + try: + 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] + device = form.save() + + # PhoneNumberForm + elif self.get_method() in ('call', 'sms'): + device = self.get_device() + device.save() + + else: + raise NotImplementedError("Unknown method '%s'" % self.get_method()) + + django_otp.login(self.request, device) + return redirect(self.success_url) + + def get_form_kwargs(self, step=None): + kwargs = {} + if step == 'generator': + kwargs.update({ + 'key': self.get_key(step), + 'user': self.request.user, + }) + if step == 'validation': + kwargs.update({ + 'device': self.get_device() + }) + metadata = self.get_form_metadata(step) + if metadata: + kwargs.update({ + 'metadata': metadata, + }) + return kwargs + + def get_device(self, **kwargs): + """ + Uses the data from the setup step and generated key to recreate device. + + Only used for call / sms -- generator uses other procedure. + """ + method = self.get_method() + kwargs = kwargs or {} + kwargs['name'] = 'default' + kwargs['user'] = self.request.user + + if method in ('call', 'sms'): + kwargs['method'] = method + kwargs['number'] = self.storage.validated_step_data\ + .get(method, {}).get('number') + return PhoneDevice(key=self.get_key(method), **kwargs) + + @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..fafa34b05 100644 --- a/two_factor/views/utils.py +++ b/two_factor/views/utils.py @@ -4,10 +4,14 @@ import logging import time +from base64 import b32encode +from binascii import unhexlify + from django.conf import settings 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 @@ -15,6 +19,10 @@ from formtools.wizard.forms import ManagementForm from formtools.wizard.storage.session import SessionStorage from formtools.wizard.views import SessionWizardView +from django.shortcuts import resolve_url +from django.urls import reverse + +from two_factor.models import random_hex_str logger = logging.getLogger(__name__) @@ -81,6 +89,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 @@ -219,6 +234,53 @@ def render_done(self, form, **kwargs): return done_response +class CustomSessionWizardView(SessionWizardView): + def get_method(self): + method_data = self.storage.validated_step_data.get('method', {}) + return method_data.get('method', None) + + def render_next_step(self, form, **kwargs): + """ + In the validation step, ask the device to generate a challenge. + """ + next_step = self.steps.next + if next_step == 'validation': + try: + self.get_device().generate_challenge() + kwargs["challenge_succeeded"] = True + except Exception: + logger.exception("Could not generate challenge") + kwargs["challenge_succeeded"] = False + return super().render_next_step(form, **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_form_metadata(self, step): + self.storage.extra_data.setdefault('forms', {}) + return self.storage.extra_data['forms'].get(step, None) + + def get_context_data(self, form, **kwargs): + context = super().get_context_data(form, **kwargs) + if self.steps.current == 'generator': + 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) + }) + elif self.steps.current == 'validation': + context['device'] = self.get_device() + context['cancel_url'] = resolve_url(settings.LOGIN_REDIRECT_URL) + return context + + def class_view_decorator(function_decorator): """ Converts a function based decorator into a class based decorator usable