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
From 5862a261380d0e94ddb35b10681fa79c28584574 Mon Sep 17 00:00:00 2001
From: Lindsay