Skip to content
Snippets Groups Projects
Commit 199f4a33 authored by Nik | Klampfradler's avatar Nik | Klampfradler
Browse files

Merge branch '91-2fa' into 'master'

Resolve "2FA"

Closes #91

See merge request BiscuIT/BiscuIT-ng!59
parents 9175a141 2f0137f8
No related branches found
No related tags found
1 merge request!59Resolve "2FA"
Pipeline #225 failed
Showing
with 378 additions and 3 deletions
from glob import glob from glob import glob
import os import os
from warnings import warn
from django.apps import AppConfig from django.apps import AppConfig, apps
from django.conf import settings from django.conf import settings
from django.db.utils import ProgrammingError
class CoreConfig(AppConfig): class CoreConfig(AppConfig):
...@@ -17,5 +19,15 @@ class CoreConfig(AppConfig): ...@@ -17,5 +19,15 @@ class CoreConfig(AppConfig):
# Ignore because old is better than nothing # Ignore because old is better than nothing
pass # noqa 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: def ready(self) -> None:
self.clean_scss() self.clean_scss()
self.setup_data()
from django.conf import settings
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
MENUS = { MENUS = {
...@@ -14,13 +15,18 @@ MENUS = { ...@@ -14,13 +15,18 @@ MENUS = {
}, },
{ {
'name': _('Login'), 'name': _('Login'),
'url': 'login', 'url': settings.LOGIN_URL,
'validators': ['menu_generator.validators.is_anonymous'] 'validators': ['menu_generator.validators.is_anonymous']
}, },
{ {
'name': _('Logout'), 'name': _('Logout'),
'url': 'logout', 'url': 'logout',
'validators': ['menu_generator.validators.is_authenticated'] '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]
} }
] ]
}, },
......
...@@ -317,4 +317,26 @@ CRON_CLASSES = [ ...@@ -317,4 +317,26 @@ CRON_CLASSES = [
ANONYMIZE_ENABLED = _settings.get('maintenance.anonymisable', True) 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__]) _settings.populate_obj(sys.modules[__name__])
{% extends "core/base.html" %}
{% block content_wrapper %}
{% block content %}{% endblock %}
{% endblock %}
{% 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>
{% load bootstrap4 %}
<div class="col-sm-12 col-md-12">
{% bootstrap_form wizard.management_form %}
{% bootstrap_form wizard.form %}
</div>
{% 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 %}
{# -*- 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 %}
{% 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 %}
{% 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 %}
{% 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 %}
{% 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 %}
{% 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 %}
...@@ -35,6 +35,14 @@ urlpatterns = [ ...@@ -35,6 +35,14 @@ urlpatterns = [
path('select2/', include('django_select2.urls')) 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 # Serve javascript-common if in development
if settings.DEBUG: if settings.DEBUG:
urlpatterns += static('/javascript/', urlpatterns += static('/javascript/',
......
...@@ -238,6 +238,17 @@ version = "1.0.0" ...@@ -238,6 +238,17 @@ version = "1.0.0"
[package.dependencies] [package.dependencies]
Django = ">1.4" 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]] [[package]]
category = "main" category = "main"
description = "Command to anonymize sensitive data." description = "Command to anonymize sensitive data."
...@@ -308,6 +319,18 @@ version = "0.1.2" ...@@ -308,6 +319,18 @@ version = "0.1.2"
[package.dependencies] [package.dependencies]
django = "*" 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]] [[package]]
category = "main" category = "main"
description = "An international phone number field for django models." description = "An international phone number field for django models."
...@@ -940,7 +963,7 @@ version = "1.25.7" ...@@ -940,7 +963,7 @@ version = "1.25.7"
ldap = ["django-auth-ldap"] ldap = ["django-auth-ldap"]
[metadata] [metadata]
content-hash = "8649e58effa8d4f96c1e0ab765ad6ab3c135648dd4bb263c0c3d2675a0000e59" content-hash = "4a10e5dc802fee8d8936df9f06c7e912d8c437035dcb53e3f13f315c8892faba"
python-versions = "^3.7" python-versions = "^3.7"
[metadata.hashes] [metadata.hashes]
...@@ -967,6 +990,7 @@ django-dbbackup = ["9470e5d8bdaee4feb878b1b66c59eb9b27a131cccd648bf7cbfe70930acd ...@@ -967,6 +990,7 @@ django-dbbackup = ["9470e5d8bdaee4feb878b1b66c59eb9b27a131cccd648bf7cbfe70930acd
django-debug-toolbar = ["24c157bc6c0e1648e0a6587511ecb1b007a00a354ce716950bff2de12693e7a8", "77cfba1d6e91b9bc3d36dc7dc74a9bb80be351948db5f880f2562a0cbf20b6c5"] django-debug-toolbar = ["24c157bc6c0e1648e0a6587511ecb1b007a00a354ce716950bff2de12693e7a8", "77cfba1d6e91b9bc3d36dc7dc74a9bb80be351948db5f880f2562a0cbf20b6c5"]
django-easy-audit = ["1c5d5e6d6a33f50f696ed53cdaf51de0a4ae2f110ef8c41b33bc139b737729a6", "4b40a30599fe721eb0a9946f5023254fa0904d531c9f4adb23ee52601efaf89b"] django-easy-audit = ["1c5d5e6d6a33f50f696ed53cdaf51de0a4ae2f110ef8c41b33bc139b737729a6", "4b40a30599fe721eb0a9946f5023254fa0904d531c9f4adb23ee52601efaf89b"]
django-fa = ["e3ebf97b90e374b5ccb5b8a70e4a932c8787f2ee995c09a97a63bf9a1366c3ff"] django-fa = ["e3ebf97b90e374b5ccb5b8a70e4a932c8787f2ee995c09a97a63bf9a1366c3ff"]
django-formtools = ["7703793f1675aa6e871f9fed147e8563816d7a5b9affdc5e3459899596217f7c", "cb2bd7c29c2104278e5a0e76f7ff256b9570acf11485d547ee0c1b35347359fb"]
django-hattori = ["6953d40881317252f19f62c4e7fe8058924b852c7498bc42beb7bc4d268c252c", "e529ed7af8fc34a0169c797c477672b687a205a56f3f5206f90c260acb83b7ac"] django-hattori = ["6953d40881317252f19f62c4e7fe8058924b852c7498bc42beb7bc4d268c252c", "e529ed7af8fc34a0169c797c477672b687a205a56f3f5206f90c260acb83b7ac"]
django-image-cropping = ["157c6f96b2bbe485bde00108cbf379ea0fcb6d7a7252648f7548aa795108dde0"] django-image-cropping = ["157c6f96b2bbe485bde00108cbf379ea0fcb6d7a7252648f7548aa795108dde0"]
django-impersonate = ["63b62d06f93b0318698c68f7314c78473914c262d4164eb66ad860bb83e04771"] django-impersonate = ["63b62d06f93b0318698c68f7314c78473914c262d4164eb66ad860bb83e04771"]
...@@ -974,6 +998,7 @@ django-ipware = ["a7c7a8fd019dbdc9c357e6e582f65034e897572fc79a7e467674efa8aef9d0 ...@@ -974,6 +998,7 @@ django-ipware = ["a7c7a8fd019dbdc9c357e6e582f65034e897572fc79a7e467674efa8aef9d0
django-maintenance-mode = ["0afcfa6ff4a87348e40c44f58f8a8c4cd3e8eca40ddcdbeb620b68ca78ecbf9c", "473850f80e7762ae586f8347129e73e0d23b89a36b98a70e0c06f1778cacff7c"] django-maintenance-mode = ["0afcfa6ff4a87348e40c44f58f8a8c4cd3e8eca40ddcdbeb620b68ca78ecbf9c", "473850f80e7762ae586f8347129e73e0d23b89a36b98a70e0c06f1778cacff7c"]
django-menu-generator = ["ce71a5055c16933c8aff64fb36c21e5cf8b6d505733aceed1252f8b99369a378"] django-menu-generator = ["ce71a5055c16933c8aff64fb36c21e5cf8b6d505733aceed1252f8b99369a378"]
django-middleware-global-request = ["f6490759bc9f7dbde4001709554e29ca715daf847f2222914b4e47117dca9313"] django-middleware-global-request = ["f6490759bc9f7dbde4001709554e29ca715daf847f2222914b4e47117dca9313"]
django-otp = ["1b6025bbbd2517b7c246828b1d11c83d53567904836ae6d57bc0058f3cd18b50", "76a698466178ce40473726ffd8c33f68d1c47f27c53f67fa4aeeb6fdde74d37b"]
django-phonenumber-field = ["1ab19f723928582fed412bd9844221fa4ff466276d8526b8b4a9913ee1487c5e", "794ebbc3068a7af75aa72a80cb0cec67e714ff8409a965968040f1fd210b2d97"] django-phonenumber-field = ["1ab19f723928582fed412bd9844221fa4ff466276d8526b8b4a9913ee1487c5e", "794ebbc3068a7af75aa72a80cb0cec67e714ff8409a965968040f1fd210b2d97"]
django-sass-processor = ["c1b56e76ce2b57382d26328ecdc204d3f65412d5da35df8a6b7bce6e7f754882"] django-sass-processor = ["c1b56e76ce2b57382d26328ecdc204d3f65412d5da35df8a6b7bce6e7f754882"]
django-select2 = ["ad12132e764ce8099bc2746e6af2f33a952b49eb63f3b062eb4739cd4304ee2f", "e4beb0e4af27f71e9e2e2f52441aecdb24d401942f18a0375031767cd0e2e5a0"] django-select2 = ["ad12132e764ce8099bc2746e6af2f33a952b49eb63f3b062eb4739cd4304ee2f", "e4beb0e4af27f71e9e2e2f52441aecdb24d401942f18a0375031767cd0e2e5a0"]
......
...@@ -50,9 +50,17 @@ django-hattori = "^0.2" ...@@ -50,9 +50,17 @@ django-hattori = "^0.2"
psycopg2 = "^2.8" psycopg2 = "^2.8"
django_select2 = "^7.1" django_select2 = "^7.1"
requests = "^2.22" 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] [tool.poetry.extras]
ldap = ["django-auth-ldap"] ldap = ["django-auth-ldap"]
2fa = ["django-otp", "django-two-factor-auth"]
twilio = ["twilio"]
yubikey = ["django-otp-yubikey"]
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
sphinx = "^2.1" sphinx = "^2.1"
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment