Skip to content

Commit

Permalink
Merge pull request dimagi#33918 from dimagi/gh/django-two-factor-auth…
Browse files Browse the repository at this point in the history
…/1.15.5

Upgrade django-two-factor-auth from 1.13.2 to 1.16.0
  • Loading branch information
gherceg authored Mar 18, 2024
2 parents 91e075c + 6f84d3a commit 416eb2f
Show file tree
Hide file tree
Showing 26 changed files with 308 additions and 115 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,18 +1,18 @@
{% load i18n two_factor %}
{% load i18n two_factor_tags %}
<form action="" method="post">{% csrf_token %}
{% include "two_factor/_wizard_forms.html" %}

{# hidden submit button to enable [enter] key #}
<div style="display: none"><input type="submit" value=""/></div>

{% if other_devices %}
{% if device.name == "default" and other_devices %}
<p>{% trans "Or, alternatively, use one of your backup phones:" %}</p>
{% for other in other_devices %}
<p>
<button name="challenge_device"
value="{{ other.persistent_id }}"
class="btn btn-default" type="submit">
{{ other|device_action }}
{{ other|as_action }}
</button>
</p>
{% endfor %}
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,51 @@
{# 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. Then, enter the token generated by the app.{% endblocktrans %}</p>
<p><img src="{{ QR_URL }}" alt="QR Code" class="bg-white"/></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 Expand Up @@ -59,17 +59,18 @@

<fieldset>
<legend>{% trans "Remove Two-Factor Authentication" %}</legend>
<p>{% blocktrans %}However strongly we discourage you to do so, you can
also <strong>remove</strong> two-factor authentication for your account.{% endblocktrans %}</p>
<p>{% blocktrans %}We <strong>strongly discourage</strong> this, but if absolutely necessary you can
remove two-factor authentication from your account.{% endblocktrans %}</p>
<p><a class="btn btn-danger" href="{% url 'two_factor:disable' %}">
{% trans "Remove Two-Factor Authentication" %}</a></p>
<br/>
</fieldset>
<fieldset>
<legend>{% trans "Reset Two-Factor Authentication" %}</legend>
<p>{% blocktrans %}
Clicking below will disable your current two-factor authentication and rerun Two-Factor Authentication setup,
so make sure you have a backup device or tokens available in case you are unable to complete the process.
This will remove your current two-factor authentication, and prompt you to run through the entire setup again.
If you need to do this, please make sure you complete the entire process once you begin, otherwise your account will
not be protected by two-factor authentication.
{% endblocktrans %}</p>
<p><a class="btn btn-default" href="{% url 'reset' %}">
{% trans "Reset Two-Factor Authentication" %}</a></p>
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 @@ -94,7 +94,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
34 changes: 34 additions & 0 deletions corehq/apps/hqwebapp/apps.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from django.apps import AppConfig
from django.conf import settings

from two_factor.plugins.registry import registry


class HqWebAppConfig(AppConfig):
Expand All @@ -7,3 +10,34 @@ class HqWebAppConfig(AppConfig):
def ready(self):
# Ensure the login signal handlers have been loaded
from . import login_handlers, signals # noqa

# custom 2FA methods need to be registered on startup
register_two_factor_methods()


def register_two_factor_methods():
from corehq.apps.hqwebapp.two_factor_methods import (
HQGeneratorMethod,
HQPhoneCallMethod,
HQSMSMethod,
)

# default generator method is registered when the registry object is created
# https://github.com/jazzband/django-two-factor-auth/blob/1.15.5/two_factor/plugins/registry.py#L76-L77
registry.unregister('generator')
registry.register(HQGeneratorMethod())

# default phone methods are registered when django starts up
# https://github.com/jazzband/django-two-factor-auth/blob/1.15.5/two_factor/plugins/phonenumber/apps.py#L19-L30
if not settings.ALLOW_PHONE_AS_DEFAULT_TWO_FACTOR_DEVICE:
registry.unregister('call')
registry.unregister('sms')
return

if getattr(settings, 'TWO_FACTOR_CALL_GATEWAY', None):
registry.unregister('call')
registry.register(HQPhoneCallMethod())

if getattr(settings, 'TWO_FACTOR_SMS_GATEWAY', None):
registry.unregister('sms')
registry.register(HQSMSMethod())
2 changes: 0 additions & 2 deletions corehq/apps/hqwebapp/decorators.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from collections import defaultdict
from functools import wraps

from django.urls import get_resolver

from corehq.apps.hqwebapp.utils.bootstrap import set_bootstrap_version5


Expand Down
46 changes: 46 additions & 0 deletions corehq/apps/hqwebapp/tests/test_apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from django.test import SimpleTestCase, override_settings

from two_factor.plugins.registry import registry

from corehq.apps.hqwebapp.apps import register_two_factor_methods
from corehq.apps.hqwebapp.two_factor_methods import (
HQGeneratorMethod,
HQPhoneCallMethod,
HQSMSMethod,
)


@override_settings(
ALLOW_PHONE_AS_DEFAULT_TWO_FACTOR_DEVICE=True,
TWO_FACTOR_SMS_GATEWAY='corehq.apps.hqwebapp.two_factor_gateways.Gateway',
TWO_FACTOR_CALL_GATEWAY='corehq.apps.hqwebapp.two_factor_gateways.Gateway',
)
class RegisterCustom2FAMethodsTests(SimpleTestCase):

@override_settings(
ALLOW_PHONE_AS_DEFAULT_TWO_FACTOR_DEVICE=False,
TWO_FACTOR_SMS_GATEWAY=None,
TWO_FACTOR_CALL_GATEWAY=None,
)
def test_custom_generator_method_is_registered(self):
register_two_factor_methods()
method = registry.get_method('generator')
self.assertIsInstance(method, HQGeneratorMethod)

@override_settings(TWO_FACTOR_SMS_GATEWAY=None)
def test_custom_call_method_is_registered_if_relevant_settings_are_true(self):
register_two_factor_methods()
method = registry.get_method('call')
self.assertIsInstance(method, HQPhoneCallMethod)

@override_settings(TWO_FACTOR_CALL_GATEWAY=None)
def test_custom_sms_method_is_registered_if_relevant_settings_are_true(self):
register_two_factor_methods()
method = registry.get_method('sms')
self.assertIsInstance(method, HQSMSMethod)

@override_settings(ALLOW_PHONE_AS_DEFAULT_TWO_FACTOR_DEVICE=False)
def test_neither_phone_method_is_registered_if_allow_phone_setting_is_false(self):
register_two_factor_methods()
self.assertIsNone(registry.get_method('call'))
self.assertIsNone(registry.get_method('sms'))
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
40 changes: 40 additions & 0 deletions corehq/apps/hqwebapp/two_factor_methods.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from django_otp.plugins.otp_totp.models import TOTPDevice

from two_factor.plugins.phonenumber.method import PhoneCallMethod, SMSMethod
from two_factor.plugins.registry import GeneratorMethod

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


class HQGeneratorMethod(GeneratorMethod):

# only overriding this because it is set in GeneratorMethod
form_path = 'corehq.apps.settings.forms.HQTOTPDeviceForm'

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

def get_token_form_class(self):
return HQAuthenticationTokenForm

def recognize_device(self, device):
return isinstance(device, TOTPDevice)


class HQPhoneCallMethod(PhoneCallMethod):

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

def get_token_form_class(self):
return HQAuthenticationTokenForm


class HQSMSMethod(SMSMethod):

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

def get_token_form_class(self):
return HQAuthenticationTokenForm
4 changes: 2 additions & 2 deletions corehq/apps/hqwebapp/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,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
8 changes: 8 additions & 0 deletions corehq/apps/hqwebapp/utils/two_factor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.conf import settings


def user_can_use_phone(user):
if not settings.ALLOW_PHONE_AS_DEFAULT_TWO_FACTOR_DEVICE:
return False

return user.belongs_to_messaging_domain()
Loading

0 comments on commit 416eb2f

Please sign in to comment.