diff --git a/biscuit/core/apps.py b/biscuit/core/apps.py index b40f987a4274c49df092a8535ce425bd848ca55f..229d3bc0937cece9d90afac436a8bcbd738b5136 100644 --- a/biscuit/core/apps.py +++ b/biscuit/core/apps.py @@ -1,8 +1,10 @@ from glob import glob import os +from warnings import warn -from django.apps import AppConfig +from django.apps import AppConfig, apps from django.conf import settings +from django.db.utils import ProgrammingError class CoreConfig(AppConfig): @@ -17,5 +19,15 @@ class CoreConfig(AppConfig): # Ignore because old is better than nothing pass # noqa + def setup_data(self) -> None: + if 'otp_yubikey' in settings.INSTALLED_APPS: + try: + apps.get_model('otp_yubikey', 'ValidationService').objects.update_or_create( + name='default', defaults={'use_ssl': True, 'param_sl': '', 'param_timeout': ''} + ) + except ProgrammingError: + warn('Yubikey validation service could not be created yet. If you are currently in a migration, this is expected.') + def ready(self) -> None: self.clean_scss() + self.setup_data() diff --git a/biscuit/core/menus.py b/biscuit/core/menus.py index d146c731827c45fb47958eaa330df7fe1738ce57..a230037f8305813cd5ec1a13307894af9da4bc15 100644 --- a/biscuit/core/menus.py +++ b/biscuit/core/menus.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.utils.translation import ugettext_lazy as _ MENUS = { @@ -14,13 +15,18 @@ MENUS = { }, { 'name': _('Login'), - 'url': 'login', + 'url': settings.LOGIN_URL, 'validators': ['menu_generator.validators.is_anonymous'] }, { 'name': _('Logout'), 'url': 'logout', 'validators': ['menu_generator.validators.is_authenticated'] + }, + { + 'name': _('Two factor auth'), + 'url': 'two_factor:profile', + 'validators': ['menu_generator.validators.is_authenticated', lambda request: 'two_factor' in settings.INSTALLED_APPS] } ] }, diff --git a/biscuit/core/settings.py b/biscuit/core/settings.py index 67c8a47d2aee762910d9b593370410ed88163372..55dfa5a30c4041e7567b16d6229dfba91f60ed1e 100644 --- a/biscuit/core/settings.py +++ b/biscuit/core/settings.py @@ -317,4 +317,26 @@ CRON_CLASSES = [ ANONYMIZE_ENABLED = _settings.get('maintenance.anonymisable', True) +if _settings.get('2fa.enabled', False): + for app in ['two_factor', 'django_otp.plugins.otp_totp', 'django_otp.plugins.otp_static', 'django_otp']: + INSTALLED_APPS.insert(INSTALLED_APPS.index('biscuit.core')+1, app) + MIDDLEWARE.insert(MIDDLEWARE.index('django.contrib.auth.middleware.AuthenticationMiddleware')+1, 'django_otp.middleware.OTPMiddleware') + + LOGIN_URL = 'two_factor:login' + + if _settings.get('2fa.yubikey.enabled', False): + INSTALLED_APPS.insert(INSTALLED_APPS.index('two_factor')+1, 'otp_yubikey') + + if _settings.get('2fa.call.enabled', False): + TWO_FACTOR_CALL_GATEWAY = 'two_factor.gateways.twilio.gateway.Twilio' + + if _settings.get('2fa.sms.enabled', False): + TWO_FACTOR_SMS_GATEWAY = 'two_factor.gateways.twilio.gateway.Twilio' + + if _settings.get('2fa.twilio.sid', None): + MIDDLEWARE.insert(MIDDLEWARE.index('django_otp.middleware.OTPMiddleware')+1, 'two_factor.middleware.threadlocals.ThreadLocals') + TWILIO_SID = _settings.get('2fa.twilio.sid') + TWILIO_TOKEN = _settings.get('2fa.twilio.token') + TWILIO_CALLER_ID = _settings.get('2fa.twilio.callerid') + _settings.populate_obj(sys.modules[__name__]) diff --git a/biscuit/core/templates/two_factor/_base_focus.html b/biscuit/core/templates/two_factor/_base_focus.html new file mode 100644 index 0000000000000000000000000000000000000000..7c5985c645ef47abb9f32c936439078a8e893d90 --- /dev/null +++ b/biscuit/core/templates/two_factor/_base_focus.html @@ -0,0 +1,5 @@ +{% extends "core/base.html" %} + +{% block content_wrapper %} + {% block content %}{% endblock %} +{% endblock %} diff --git a/biscuit/core/templates/two_factor/_wizard_actions.html b/biscuit/core/templates/two_factor/_wizard_actions.html new file mode 100644 index 0000000000000000000000000000000000000000..c6bfcce94adc95c88a610bc5b9ea95f9fa6778e3 --- /dev/null +++ b/biscuit/core/templates/two_factor/_wizard_actions.html @@ -0,0 +1,15 @@ +{% load i18n %} + +{% if cancel_url %} + <a href="{{ cancel_url }}" + class="pull-right btn btn-dark">{% trans "Cancel" %}</a> +{% endif %} +{% if wizard.steps.prev %} + <button name="wizard_goto_step" type="submit" + value="{{ wizard.steps.prev }}" + class="btn btn-dark">{% trans "Back" %}</button> +{% else %} + <button disabled name="" type="button" + class="btn btn-disabled">{% trans "Back" %}</button> +{% endif %} +<button type="submit" class="btn btn-dark">{% trans "Next" %}</button> diff --git a/biscuit/core/templates/two_factor/_wizard_forms.html b/biscuit/core/templates/two_factor/_wizard_forms.html new file mode 100644 index 0000000000000000000000000000000000000000..a5fd6e55b2d6c1afffe5f93df91b2edb6ce3ee32 --- /dev/null +++ b/biscuit/core/templates/two_factor/_wizard_forms.html @@ -0,0 +1,6 @@ +{% load bootstrap4 %} + +<div class="col-sm-12 col-md-12"> + {% bootstrap_form wizard.management_form %} + {% bootstrap_form wizard.form %} +</div> diff --git a/biscuit/core/templates/two_factor/core/backup_tokens.html b/biscuit/core/templates/two_factor/core/backup_tokens.html new file mode 100644 index 0000000000000000000000000000000000000000..1e64a9e4405ece599e35f976abd4fa8461c10809 --- /dev/null +++ b/biscuit/core/templates/two_factor/core/backup_tokens.html @@ -0,0 +1,28 @@ +{% extends "core/base.html" %} +{% load i18n %} + +{% block content %} + <h1>{% block title %}{% trans "Backup Tokens" %}{% endblock %}</h1> + <p>{% blocktrans %}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 %}</p> + + {% if device.token_set.count %} + <ul> + {% for token in device.token_set.all %} + <li>{{ token.token }}</li> + {% endfor %} + </ul> + <p>{% blocktrans %}Print these tokens and keep them somewhere safe.{% endblocktrans %}</p> + {% else %} + <p>{% trans "You don't have any backup codes yet." %}</p> + {% endif %} + + <form method="post">{% csrf_token %}{{ form }} + <a href="{% url 'two_factor:profile'%}" + class="pull-right btn btn-dark">{% trans "Back to Account Security" %}</a> + <button class="btn btn-dark" type="submit">{% trans "Generate Tokens" %}</button> + </form> +{% endblock %} diff --git a/biscuit/core/templates/two_factor/core/login.html b/biscuit/core/templates/two_factor/core/login.html new file mode 100644 index 0000000000000000000000000000000000000000..083443d305d2916c04360d10b158d3c190171478 --- /dev/null +++ b/biscuit/core/templates/two_factor/core/login.html @@ -0,0 +1,53 @@ +{# -*- engine:django -*- #} +{% extends "two_factor/_base_focus.html" %} +{% load i18n two_factor %} + +{% block content %} + <h1>{% block title %}{% trans "Login" %}{% endblock %}</h1> + + {% if wizard.steps.current == 'auth' %} + <p>{% blocktrans %}Enter your credentials.{% endblocktrans %}</p> + {% elif wizard.steps.current == 'token' %} + {% 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> + {% else %} + <p>{% blocktrans %}Please enter the tokens generated by your token + generator.{% endblocktrans %}</p> + {% endif %} + {% elif wizard.steps.current == 'backup' %} + <p>{% blocktrans %}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 %}</p> + {% endif %} + + <form action="" method="post">{% csrf_token %} + {% include "two_factor/_wizard_forms.html" %} + + {# hidden submit button to enable [enter] key #} + <div style="margin-left: -9999px"><input type="submit" value=""/></div> + + {% if other_devices %} + <p>{% trans "Or, alternatively, use one of your backup phones:" %}</p> + <p> + {% for other in other_devices %} + <button name="challenge_device" value="{{ other.persistent_id }}" + class="btn btn-dark btn-block" type="submit"> + {{ other|device_action }} + </button> + {% endfor %}</p> + {% endif %} + {% if backup_tokens %} + <p>{% trans "As a last resort, you can use a backup token:" %}</p> + <p> + <button name="wizard_goto_step" type="submit" value="backup" + class="btn btn-dark btn-block">{% trans "Use Backup Token" %}</button> + </p> + {% endif %} + + {% include "two_factor/_wizard_actions.html" %} + </form> +{% endblock %} diff --git a/biscuit/core/templates/two_factor/core/otp_required.html b/biscuit/core/templates/two_factor/core/otp_required.html new file mode 100644 index 0000000000000000000000000000000000000000..0ed49da0ac083b2ae71e7c048d1d46b57fb466fc --- /dev/null +++ b/biscuit/core/templates/two_factor/core/otp_required.html @@ -0,0 +1,20 @@ +{% extends "core/base.html" %} +{% load i18n %} + +{% block content %} + <h1>{% block title %}{% trans "Permission Denied" %}{% endblock %}</h1> + + <p>{% blocktrans %}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 %}</p> + + <p>{% blocktrans %}Two-factor authentication is not enabled for your + account. Enable two-factor authentication for enhanced account + security.{% endblocktrans %}</p> + <p> + <a href="javascript:history.go(-1)" + class="pull-right btn btn-dark">{% trans "Go back" %}</a> + <a href="{% url 'two_factor:setup' %}" class="btn btn-dark"> + {% trans "Enable Two-Factor Authentication" %}</a> + </p> +{% endblock %} diff --git a/biscuit/core/templates/two_factor/core/phone_register.html b/biscuit/core/templates/two_factor/core/phone_register.html new file mode 100644 index 0000000000000000000000000000000000000000..304ef4674f35b6d259c7d568f90845a08bd26d85 --- /dev/null +++ b/biscuit/core/templates/two_factor/core/phone_register.html @@ -0,0 +1,24 @@ +{% extends "two_factor/_base_focus.html" %} +{% load i18n %} + +{% block content %} + <h1>{% block title %}{% trans "Add Backup Phone" %}{% endblock %}</h1> + + {% if wizard.steps.current == 'setup' %} + <p>{% blocktrans %}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 %}</p> + {% elif wizard.steps.current == 'validation' %} + <p>{% blocktrans %}We've sent a token to your phone number. Please + enter the token you've received.{% endblocktrans %}</p> + {% endif %} + + <form action="" method="post">{% csrf_token %} + {% include "two_factor/_wizard_forms.html" %} + + {# hidden submit button to enable [enter] key #} + <div style="margin-left: -9999px"><input type="submit" value=""/></div> + + {% include "two_factor/_wizard_actions.html" %} + </form> +{% endblock %} diff --git a/biscuit/core/templates/two_factor/core/setup.html b/biscuit/core/templates/two_factor/core/setup.html new file mode 100644 index 0000000000000000000000000000000000000000..18fcaade6a39035a6af9c03aa58028b7d09c3d79 --- /dev/null +++ b/biscuit/core/templates/two_factor/core/setup.html @@ -0,0 +1,55 @@ +{% extends "two_factor/_base_focus.html" %} +{% load i18n %} + +{% block content %} + <h1>{% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}</h1> + {% if wizard.steps.current == 'welcome' %} + <p>{% blocktrans %}You are about to take your account security to the + next level. 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> + {% 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> + {% elif wizard.steps.current == 'sms' %} + <p>{% blocktrans %}Please enter the phone number you wish to receive the + text messages on. This number will be validated in the next step. + {% endblocktrans %}</p> + {% elif wizard.steps.current == 'call' %} + <p>{% blocktrans %}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 challenge_succeeded %} + {% 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> + {% endif %} + {% else %} + <p class="alert alert-warning" role="alert">{% blocktrans %}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 + token in the field below. Your YubiKey will be linked to your + account.{% endblocktrans %}</p> + {% endif %} + <form action="" method="post">{% csrf_token %} + {% include "two_factor/_wizard_forms.html" %} + + {# hidden submit button to enable [enter] key #} + <div style="margin-left: -9999px"><input type="submit" value=""/></div> + + {% include "two_factor/_wizard_actions.html" %} + </form> +{% endblock %} diff --git a/biscuit/core/templates/two_factor/core/setup_complete.html b/biscuit/core/templates/two_factor/core/setup_complete.html new file mode 100644 index 0000000000000000000000000000000000000000..762423f006c10360aac056c7013eab4367f39af7 --- /dev/null +++ b/biscuit/core/templates/two_factor/core/setup_complete.html @@ -0,0 +1,25 @@ +{% extends "two_factor/_base_focus.html" %} +{% load i18n %} + +{% block content %} + <h1>{% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}</h1> + + <p>{% blocktrans %}Congratulations, you've successfully enabled two-factor + authentication.{% endblocktrans %}</p> + + {% if not phone_methods %} + <a href="{% url 'two_factor:profile' %}" class="pull-left btn btn-dark">{% trans "Back to Profile" %}</a> + <a href="{% url 'two_factor:backup_tokens' %}" class="pull-right btn btn-dark">{% trans "Generate backup codes" %}</a> + {% else %} + <p>{% blocktrans %}However, it might happen that you don't have access to + your primary token device. To enable account recovery, generate backup codes + or add a phone number.{% endblocktrans %}</p> + <a href="{% url 'two_factor:profile' %}" + class="pull-right btn btn-dark">{% trans "Back to Profile" %}</a> + <a href="{% url 'two_factor:backup_tokens' %}" + class="pull-right btn btn-dark">{% trans "Generate backup codes" %}</a> + <a href="{% url 'two_factor:phone_create' %}" + class="btn btn-success">{% trans "Add Phone Number" %}</a> + {% endif %} + +{% endblock %} diff --git a/biscuit/core/templates/two_factor/profile/profile.html b/biscuit/core/templates/two_factor/profile/profile.html new file mode 100644 index 0000000000000000000000000000000000000000..f3d3afcc8164578423f09feb3b1138701104d094 --- /dev/null +++ b/biscuit/core/templates/two_factor/profile/profile.html @@ -0,0 +1,63 @@ +{% extends "two_factor/_base_focus.html" %} +{% load i18n two_factor %} + +{% block content %} + <h1>{% block title %}{% trans "Account Security" %}{% endblock %}</h1> + + {% if default_device %} + {% if default_device_type == 'TOTPDevice' %} + <p>{% trans "Tokens will be generated by your token generator." %}</p> + {% elif default_device_type == 'PhoneDevice' %} + <p>{% blocktrans with primary=default_device|device_action %}Primary method: {{ primary }}{% endblocktrans %}</p> + {% elif default_device_type == 'RemoteYubikeyDevice' %} + <p>{% blocktrans %}Tokens will be generated by your YubiKey.{% endblocktrans %}</p> + {% endif %} + + {% if available_phone_methods %} + <h2>{% trans "Backup Phone Numbers" %}</h2> + <p>{% blocktrans %}If your primary method is not available, we are able to + send backup tokens to the phone numbers listed below.{% endblocktrans %}</p> + <ul> + {% for phone in backup_phones %} + <li> + {{ phone|device_action }} + <form method="post" action="{% url 'two_factor:phone_delete' phone.id %}" + onsubmit="return confirm('Are you sure?')"> + {% csrf_token %} + <button class="btn btn-warning" + type="submit">{% trans "Unregister" %}</button> + </form> + </li> + {% endfor %} + </ul> + <p><a href="{% url 'two_factor:phone_create' %}" + class="btn btn-info">{% trans "Add Phone Number" %}</a></p> + {% endif %} + + <h2>{% trans "Backup Tokens" %}</h2> + <p> + {% blocktrans %}If you don't have any device with you, you can access + your account using backup tokens.{% endblocktrans %} + {% blocktrans count counter=backup_tokens %} + You have only one backup token remaining. + {% plural %} + You have {{ counter }} backup tokens remaining. + {% endblocktrans %} + </p> + <p><a href="{% url 'two_factor:backup_tokens' %}" + class="btn btn-dark">{% trans "Show Codes" %}</a></p> + + <h3>{% trans "Disable Two-Factor Authentication" %}</h3> + <p>{% blocktrans %}However we strongly discourage you to do so, you can + also disable two-factor authentication for your account.{% endblocktrans %}</p> + <p><a class="btn btn-dark" href="{% url 'two_factor:disable' %}"> + {% trans "Disable Two-Factor Authentication" %}</a></p> + {% else %} + <p>{% blocktrans %}Two-factor authentication is not enabled for your + account. Enable two-factor authentication for enhanced account + security.{% endblocktrans %}</p> + <p><a href="{% url 'two_factor:setup' %}" class="btn btn-dark"> + {% trans "Enable Two-Factor Authentication" %}</a> + </p> + {% endif %} +{% endblock %} diff --git a/biscuit/core/urls.py b/biscuit/core/urls.py index 794394a34baecd64f3744140ba2922e592c7d571..62bd0792cc3d72f4ae911e96e87902d409097bca 100644 --- a/biscuit/core/urls.py +++ b/biscuit/core/urls.py @@ -35,6 +35,14 @@ urlpatterns = [ path('select2/', include('django_select2.urls')) ] +# Add URLs for optional features +if 'two_factor' in settings.INSTALLED_APPS: + from two_factor.urls import urlpatterns as tf_urls # noqa + urlpatterns += [path('', include(tf_urls))] +if hasattr(settings, 'TWILIO_ACCOUNT_SID'): + from two_factor.gateways.twilio.urls import urlpatterns as tf_twilio_urls # noqa + urlpatterns += [path('', include(tf_twilio_urls))] + # Serve javascript-common if in development if settings.DEBUG: urlpatterns += static('/javascript/', diff --git a/poetry.lock b/poetry.lock index 0913c6c8819f177e9ae9a0e9e9bc740e36f9e1e1..5e24fb5fdcf7dcdbd854afd9ef46aa89ca79be4b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -238,6 +238,17 @@ version = "1.0.0" [package.dependencies] Django = ">1.4" +[[package]] +category = "main" +description = "A set of high-level abstractions for Django forms" +name = "django-formtools" +optional = false +python-versions = "*" +version = "2.1" + +[package.dependencies] +Django = ">=1.8" + [[package]] category = "main" description = "Command to anonymize sensitive data." @@ -308,6 +319,18 @@ version = "0.1.2" [package.dependencies] django = "*" +[[package]] +category = "main" +description = "A pluggable framework for adding two-factor authentication to Django using one-time passwords." +name = "django-otp" +optional = false +python-versions = "*" +version = "0.7.4" + +[package.dependencies] +django = ">=1.11" +six = ">=1.10.0" + [[package]] category = "main" description = "An international phone number field for django models." @@ -940,7 +963,7 @@ version = "1.25.7" ldap = ["django-auth-ldap"] [metadata] -content-hash = "8649e58effa8d4f96c1e0ab765ad6ab3c135648dd4bb263c0c3d2675a0000e59" +content-hash = "4a10e5dc802fee8d8936df9f06c7e912d8c437035dcb53e3f13f315c8892faba" python-versions = "^3.7" [metadata.hashes] @@ -967,6 +990,7 @@ django-dbbackup = ["9470e5d8bdaee4feb878b1b66c59eb9b27a131cccd648bf7cbfe70930acd django-debug-toolbar = ["24c157bc6c0e1648e0a6587511ecb1b007a00a354ce716950bff2de12693e7a8", "77cfba1d6e91b9bc3d36dc7dc74a9bb80be351948db5f880f2562a0cbf20b6c5"] django-easy-audit = ["1c5d5e6d6a33f50f696ed53cdaf51de0a4ae2f110ef8c41b33bc139b737729a6", "4b40a30599fe721eb0a9946f5023254fa0904d531c9f4adb23ee52601efaf89b"] django-fa = ["e3ebf97b90e374b5ccb5b8a70e4a932c8787f2ee995c09a97a63bf9a1366c3ff"] +django-formtools = ["7703793f1675aa6e871f9fed147e8563816d7a5b9affdc5e3459899596217f7c", "cb2bd7c29c2104278e5a0e76f7ff256b9570acf11485d547ee0c1b35347359fb"] django-hattori = ["6953d40881317252f19f62c4e7fe8058924b852c7498bc42beb7bc4d268c252c", "e529ed7af8fc34a0169c797c477672b687a205a56f3f5206f90c260acb83b7ac"] django-image-cropping = ["157c6f96b2bbe485bde00108cbf379ea0fcb6d7a7252648f7548aa795108dde0"] django-impersonate = ["63b62d06f93b0318698c68f7314c78473914c262d4164eb66ad860bb83e04771"] @@ -974,6 +998,7 @@ django-ipware = ["a7c7a8fd019dbdc9c357e6e582f65034e897572fc79a7e467674efa8aef9d0 django-maintenance-mode = ["0afcfa6ff4a87348e40c44f58f8a8c4cd3e8eca40ddcdbeb620b68ca78ecbf9c", "473850f80e7762ae586f8347129e73e0d23b89a36b98a70e0c06f1778cacff7c"] django-menu-generator = ["ce71a5055c16933c8aff64fb36c21e5cf8b6d505733aceed1252f8b99369a378"] django-middleware-global-request = ["f6490759bc9f7dbde4001709554e29ca715daf847f2222914b4e47117dca9313"] +django-otp = ["1b6025bbbd2517b7c246828b1d11c83d53567904836ae6d57bc0058f3cd18b50", "76a698466178ce40473726ffd8c33f68d1c47f27c53f67fa4aeeb6fdde74d37b"] django-phonenumber-field = ["1ab19f723928582fed412bd9844221fa4ff466276d8526b8b4a9913ee1487c5e", "794ebbc3068a7af75aa72a80cb0cec67e714ff8409a965968040f1fd210b2d97"] django-sass-processor = ["c1b56e76ce2b57382d26328ecdc204d3f65412d5da35df8a6b7bce6e7f754882"] django-select2 = ["ad12132e764ce8099bc2746e6af2f33a952b49eb63f3b062eb4739cd4304ee2f", "e4beb0e4af27f71e9e2e2f52441aecdb24d401942f18a0375031767cd0e2e5a0"] diff --git a/pyproject.toml b/pyproject.toml index cab8931d2cedccbb4ef4ec45dc470869f2d19946..e793a460f5369986d66053709baee70990977212 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,9 +50,17 @@ django-hattori = "^0.2" psycopg2 = "^2.8" django_select2 = "^7.1" requests = "^2.22" +django-formtools = "^2.1" +django-otp = { version = "^0.7.4", optional = true } +django-two-factor-auth = { version = "^1.9", optional = true } +django-otp-yubikey = { version = '^0.5.2', optional = true } +twilio = { version = "^6.33", optional = true } [tool.poetry.extras] ldap = ["django-auth-ldap"] +2fa = ["django-otp", "django-two-factor-auth"] +twilio = ["twilio"] +yubikey = ["django-otp-yubikey"] [tool.poetry.dev-dependencies] sphinx = "^2.1"