Skip to content

Commit

Permalink
Merge branch 'gh/django/upgrade-two-factor-auth' into autostaging
Browse files Browse the repository at this point in the history
  • Loading branch information
gherceg committed Dec 26, 2023
2 parents 879952e + 6d10fa8 commit c43a1cb
Show file tree
Hide file tree
Showing 20 changed files with 117 additions and 91 deletions.
2 changes: 1 addition & 1 deletion corehq/apps/domain/deletion.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ def _delete_demo_user_restores(domain_name):
DOMAIN_DELETE_OPERATIONS = [
DjangoUserRelatedModelDeletion('otp_static', 'StaticDevice', 'user__username', ['StaticToken']),
DjangoUserRelatedModelDeletion('otp_totp', 'TOTPDevice', 'user__username'),
DjangoUserRelatedModelDeletion('two_factor', 'PhoneDevice', 'user__username'),
DjangoUserRelatedModelDeletion('phonenumber', 'PhoneDevice', 'user__username'),
DjangoUserRelatedModelDeletion('users', 'HQApiKey', 'user__username'),
CustomDeletion('auth', _delete_django_users, ['User']),
ModelDeletion('products', 'SQLProduct', 'domain'),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% load i18n two_factor %}
{% load i18n phonenumber %}

{% block focus-content %}
<h2 class="text-center">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% load i18n two_factor %}
{% load i18n phonenumber %}
<form action="" method="post">{% csrf_token %}
{% include "two_factor/_wizard_forms.html" %}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
{# lightly modified version of two_factor/core/phone_register.html #}
{% extends "hqwebapp/bootstrap3/base_section.html" %}
{% load i18n %}
{% load crispy_forms_tags %}

{% block page_content %}
<h1>{% block title %}{% trans "Add Backup Phone" %}{% endblock %}</h1>

{% if wizard.steps.current == 'method' %}
<p>{% blocktrans %}Your backup phone number will be used if your primary method of
{% if wizard.steps.current == 'setup' %}
<p>{% blocktrans trimmed %}Your backup phone number will be used if your primary method of
registration is not available. Please enter a valid phone number.{% endblocktrans %}</p>
{% elif wizard.steps.current == 'validation' %}
<p>{% blocktrans %}We've sent a token to your phone number. Please
<p>{% blocktrans trimmed %}We've sent a token to your phone number. Please
enter the token you've received.{% endblocktrans %}</p>
{% endif %}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,55 @@
{# lightly modified version of two_factor/core/setup.html #}
{% extends "hqwebapp/bootstrap3/base_section.html" %}
{% load i18n %}
{% load crispy_forms_tags %}

{% block extra_media %}
{{ form.media }}
{% endblock %}

{% block page_content %}
<h1>{% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}</h1>
{% if wizard.steps.current == 'welcome_setup' %}
<p>{% blocktrans %}Follow the steps in this wizard to enable two-factor
authentication.{% endblocktrans %}</p>
{% elif wizard.steps.current == 'welcome_reset' %}
<p>{% blocktrans %}Follow the steps in this wizard to reset two-factor
authentication.{% endblocktrans %}</p>
{% if wizard.steps.current == 'welcome' %}
<p>{% blocktrans trimmed %}Follow the steps in this wizard to enable two-factor
authentication.{% endblocktrans %}</p>
{% elif wizard.steps.current == 'method' %}
<p>{% blocktrans %}Please select which authentication method you would
like to use.{% endblocktrans %}</p>
<p>{% blocktrans trimmed %}Please select which authentication method you would
like to use.{% endblocktrans %}</p>
{% elif wizard.steps.current == 'generator' %}
<p>{% 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 %}</p>
<p><img src="{{ QR_URL }}" alt="QR Code" /></p>
<p>{% blocktrans trimmed %}To start using a token generator, please use your
smartphone to scan the QR code below. For example, use Google
Authenticator.{% endblocktrans %}</p>
<p><img src="{{ QR_URL }}" alt="QR Code" class="bg-white"/></p>
<p>{% blocktrans trimmed %}Alternatively you can use the following secret to
setup TOTP in your authenticator or password manager manually.{% endblocktrans %}</p>
<p>{% translate "TOTP Secret:" %} <a href="{{ otpauth_url }}">{{ secret_key }}</a></p>
<p>{% blocktrans %}Then, enter the token generated by the app.{% endblocktrans %}</p>

{% elif wizard.steps.current == 'sms' %}
<p>{% blocktrans %}Please enter the phone number you wish to receive the
<p>{% 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 %}</p>
{% endblocktrans %}</p>
{% elif wizard.steps.current == 'call' %}
<p>{% blocktrans %}Please enter the phone number you wish to be called on.
<p>{% blocktrans trimmed %}Please enter the phone number you wish to be called on.
This number will be validated in the next step. {% endblocktrans %}</p>
{% elif wizard.steps.current == 'validation' %}
{% if device.method == 'call' %}
<p>{% blocktrans %}We are calling your phone right now, please enter the
digits you hear.{% endblocktrans %}</p>
{% elif device.method == 'sms' %}
<p>{% blocktrans %}We sent you a text message, please enter the tokens we
sent.{% endblocktrans %}</p>
{% if challenge_succeeded %}
{% if device.method == 'call' %}
<p>{% blocktrans trimmed %}We are calling your phone right now, please enter the
digits you hear.{% endblocktrans %}</p>
{% elif device.method == 'sms' %}
<p>{% blocktrans trimmed %}We sent you a text message, please enter the tokens we
sent.{% endblocktrans %}</p>
{% endif %}
{% else %}
<p class="alert alert-warning" role="alert">{% 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 %}</p>
{% endif %}
{% elif wizard.steps.current == 'yubikey' %}
<p>{% blocktrans %}To identify and verify your YubiKey, please insert a
<p>{% 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 %}</p>
{% endif %}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% extends "hqwebapp/bootstrap3/base_section.html" %}
{% load i18n two_factor %}
{% load i18n phonenumber %}

{% block page_content %}
{% if is_using_sso %}
Expand Down
2 changes: 1 addition & 1 deletion corehq/apps/dump_reload/tests/test_dump_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
"tastypie.ApiAccess", # not tagged by domain
"tastypie.ApiKey", # not domain-specific
"toggle_ui.ToggleAudit",
"two_factor.PhoneDevice",
"phonenumber.PhoneDevice",
"users.Permission",
"util.BouncedEmail",
"util.ComplaintBounceMeta",
Expand Down
2 changes: 1 addition & 1 deletion corehq/apps/hqwebapp/two_factor_gateways.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from twilio.base.exceptions import TwilioRestException
from twilio.http.http_client import TwilioHttpClient
from twilio.rest import Client
from two_factor.models import PhoneDevice
from two_factor.plugins.phonenumber.models import PhoneDevice

import settings
from corehq.apps.users.models import CouchUser
Expand Down
4 changes: 2 additions & 2 deletions corehq/apps/hqwebapp/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@
url(r'^account/two_factor/backup/tokens/$', TwoFactorBackupTokensView.as_view(),
name=TwoFactorBackupTokensView.urlname),
url(r'^account/two_factor/disable/$', TwoFactorDisableView.as_view(), name=TwoFactorDisableView.urlname),
url(r'^account/two_factor/backup/phone/register/$', TwoFactorPhoneSetupView.as_view(),
url(r'^account/two_factor/phone/register/$', TwoFactorPhoneSetupView.as_view(),
name=TwoFactorPhoneSetupView.urlname),
url(r'^account/two_factor/backup/phone/unregister/(?P<pk>\d+)/$', TwoFactorPhoneDeleteView.as_view(),
url(r'^account/two_factor/phone/unregister/(?P<pk>\d+)/$', TwoFactorPhoneDeleteView.as_view(),
name=TwoFactorPhoneDeleteView.urlname),
url(r'', include(tf_urls)),
url(r'', include(tf_twilio_urls)),
Expand Down
23 changes: 8 additions & 15 deletions corehq/apps/settings/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@
from two_factor.forms import (
DeviceValidationForm,
MethodForm,
TOTPDeviceForm,
)
from two_factor.plugins.phonenumber.forms import (
PhoneNumberForm,
PhoneNumberMethodForm,
TOTPDeviceForm,
)
from two_factor.models import get_available_phone_methods
from two_factor.utils import totp_digits

from corehq.apps.hqwebapp import crispy as hqcrispy
Expand Down Expand Up @@ -140,16 +141,8 @@ def clean_token(self):

class HQTwoFactorMethodForm(MethodForm):

def __init__(self, *, allow_phone_2fa, **kwargs):
super().__init__(**kwargs)
if not allow_phone_2fa:
# Block people from setting up the phone method as their default
phone_methods = [method for method, _ in get_available_phone_methods()]
self.fields['method'].choices = [
(method, display_name)
for method, display_name in self.fields['method'].choices
if method not in phone_methods
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_class = 'form form-horizontal'
self.helper.label_class = 'col-sm-3 col-md-4 col-lg-2'
Expand All @@ -170,7 +163,7 @@ def __init__(self, *, allow_phone_2fa, **kwargs):
_("Back"),
css_class='btn-default',
type='submit',
value='welcome_setup',
value='welcome',
name="wizard_goto_step",
),
)
Expand All @@ -180,8 +173,8 @@ def __init__(self, *, allow_phone_2fa, **kwargs):
class HQTOTPDeviceForm(TOTPDeviceForm):
token = forms.IntegerField(required=False, label=_("Token"), min_value=1, max_value=int('9' * totp_digits()))

def __init__(self, **kwargs):
super(HQTOTPDeviceForm, self).__init__(**kwargs)
def __init__(self, key, user, **kwargs):
super(HQTOTPDeviceForm, self).__init__(key, user, **kwargs)
self.helper = FormHelper()
self.helper.form_class = 'form form-horizontal'
self.helper.label_class = 'col-sm-3 col-md-4 col-lg-2'
Expand Down
28 changes: 28 additions & 0 deletions corehq/apps/settings/method.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from two_factor.plugins.phonenumber.method import PhoneCallMethod, SMSMethod
from two_factor.plugins.registry import GeneratorMethod

from corehq.apps.settings.forms import HQPhoneNumberForm, HQTOTPDeviceForm


class HQGeneratorMethod(GeneratorMethod):

form_path = 'corehq.apps.settings.forms.HQTOTPDeviceForm'

def get_setup_forms(self, *args):
return {self.code: HQTOTPDeviceForm}


class HQPhoneCallMethod(PhoneCallMethod):

form_path = 'corehq.apps.settings.forms.HQPhoneNumberForm'

def get_setup_forms(self, *args):
return {self.code: HQPhoneNumberForm}


class HQSMSMethod(SMSMethod):

form_path = 'corehq.apps.settings.forms.HQPhoneNumberForm'

def get_setup_forms(self, *args):
return {self.code: HQPhoneNumberForm}
3 changes: 2 additions & 1 deletion corehq/apps/settings/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
from django.test.client import RequestFactory
from django.urls import reverse

from two_factor.views import PhoneSetupView, ProfileView, SetupView
from two_factor.views import ProfileView, SetupView
from two_factor.plugins.phonenumber.views import PhoneSetupView

from corehq.apps.domain.shortcuts import create_domain
from corehq.apps.users.audit.change_messages import UserChangeMessage
Expand Down
50 changes: 21 additions & 29 deletions corehq/apps/settings/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,21 @@
import langcodes
import qrcode
from memoized import memoized
from two_factor.models import PhoneDevice
from two_factor.utils import backup_phones, default_device
from two_factor.plugins.phonenumber.models import PhoneDevice
from two_factor.utils import default_device
from two_factor.plugins.phonenumber.utils import backup_phones
from two_factor.views import (
BackupTokensView,
DisableView,
PhoneDeleteView,
PhoneSetupView,
ProfileView,
SetupCompleteView,
SetupView,
)
from two_factor.plugins.phonenumber.views import (
PhoneDeleteView,
PhoneSetupView
)
from two_factor.plugins.registry import registry

from dimagi.utils.web import json_response

Expand All @@ -54,14 +58,13 @@
not_found,
)
from corehq.apps.settings.exceptions import DuplicateApiKeyName
from corehq.apps.settings.method import HQGeneratorMethod, HQPhoneCallMethod, HQSMSMethod
from corehq.apps.settings.forms import (
HQApiKeyForm,
HQDeviceValidationForm,
HQEmptyForm,
HQPasswordChangeForm,
HQPhoneNumberForm,
HQPhoneNumberMethodForm,
HQTOTPDeviceForm,
HQTwoFactorMethodForm,
)
from corehq.apps.sso.models import IdentityProvider
Expand Down Expand Up @@ -406,26 +409,21 @@ class TwoFactorSetupView(BaseMyAccountView, SetupView):
page_title = gettext_lazy("Two Factor Authentication Setup")

form_list = (
('welcome_setup', HQEmptyForm),
('welcome', HQEmptyForm),
('method', HQTwoFactorMethodForm),
('generator', HQTOTPDeviceForm),
('sms', HQPhoneNumberForm),
('call', HQPhoneNumberForm),
('validation', HQDeviceValidationForm),
# other forms are dynamically added
)

@method_decorator(active_domains_required)
@method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
# this is only here to add the login_required decorator
return super(TwoFactorSetupView, self).dispatch(request, *args, **kwargs)

def get_form_kwargs(self, step=None):
kwargs = super().get_form_kwargs(step)
if step == 'method':
kwargs.setdefault('allow_phone_2fa', _user_can_use_phone(self.request.couch_user))
registry.unregister('generator')
registry.register(HQGeneratorMethod())
if _user_can_use_phone(request.user):
registry.register(HQSMSMethod())
registry.register(HQPhoneCallMethod())

return kwargs
return super(TwoFactorSetupView, self).dispatch(request, *args, **kwargs)


class TwoFactorSetupCompleteView(BaseMyAccountView, SetupCompleteView):
Expand Down Expand Up @@ -492,7 +490,7 @@ class TwoFactorPhoneSetupView(BaseMyAccountView, PhoneSetupView):
page_title = gettext_lazy("Two Factor Authentication Phone Setup")

form_list = (
('method', HQPhoneNumberMethodForm),
('setup', HQPhoneNumberMethodForm),
('validation', HQDeviceValidationForm),
)

Expand Down Expand Up @@ -541,17 +539,11 @@ def dispatch(self, request, *args, **kwargs):
class TwoFactorResetView(TwoFactorSetupView):
urlname = 'reset'

form_list = (
('welcome_reset', HQEmptyForm),
('method', HQTwoFactorMethodForm),
('generator', HQTOTPDeviceForm),
('sms', HQPhoneNumberForm),
('call', HQPhoneNumberForm),
('validation', HQDeviceValidationForm),
)

def get(self, request, *args, **kwargs):
default_device(request.user).delete()
# django-two-factor-auth caches the default device on the user so clear that too
from two_factor.utils import USER_DEFAULT_DEVICE_ATTR_NAME
delattr(request.user, USER_DEFAULT_DEVICE_ATTR_NAME)
return super(TwoFactorResetView, self).get(request, *args, **kwargs)


Expand Down
10 changes: 3 additions & 7 deletions migrations.lock
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,8 @@ phonelog
0012_server_date_not_null
0013_delete_olddevicereportentry
0014_auto_20170718_2039
phonenumber
0001_squashed_0001_initial (10 squashed migrations)
pillow_retry
0001_initial
0002_pillowerror_queued
Expand Down Expand Up @@ -1097,13 +1099,7 @@ translations
0008_remove_couch_id
0009_auto_20200924_1753
two_factor
0001_initial
0002_auto_20150110_0810
0003_auto_20150817_1733
0004_auto_20160205_1827
0005_auto_20160224_0450
0006_phonedevice_key_default
0007_auto_20201201_1019
(no migrations)
user_importer
0001_initial
userreports
Expand Down
2 changes: 1 addition & 1 deletion requirements/dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ django-tastypie==0.14.4
# via -r base-requirements.in
django-transfer==0.4
# via -r base-requirements.in
django-two-factor-auth==1.13.2
django-two-factor-auth==1.15.5
# via -r base-requirements.in
django-user-agents==0.4.0
# via -r base-requirements.in
Expand Down
2 changes: 1 addition & 1 deletion requirements/docs-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ django-tastypie==0.14.4
# via -r base-requirements.in
django-transfer==0.4
# via -r base-requirements.in
django-two-factor-auth==1.13.2
django-two-factor-auth==1.15.5
# via -r base-requirements.in
django-user-agents==0.4.0
# via -r base-requirements.in
Expand Down
Loading

0 comments on commit c43a1cb

Please sign in to comment.