diff --git a/requirements/base.in b/requirements/base.in index df9c8322..ea7ea985 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -13,8 +13,7 @@ django-axes django-jsonsuit django-redis django-rosetta -maykin-django-two-factor-auth -maykin-django-two-factor-auth[phonenumbers] +maykin-2fa mozilla-django-oidc-db sharing-configs diff --git a/requirements/base.txt b/requirements/base.txt index d5cbd282..71967b9d 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -8,6 +8,8 @@ amqp==5.1.1 # via kombu asgiref==3.6.0 # via django +asn1crypto==1.5.1 + # via webauthn async-timeout==4.0.2 # via redis attrs==23.1.0 @@ -20,6 +22,8 @@ boltons==23.0.0 # via # face # glom +cbor2==5.6.1 + # via webauthn celery==5.2.7 # via notifications-api-common certifi==2023.5.7 @@ -50,12 +54,13 @@ coreapi==2.3.3 # via commonground-api-common coreschema==0.0.4 # via coreapi -cryptography==40.0.2 +cryptography==42.0.2 # via # django-simple-certmanager # josepy # mozilla-django-oidc # pyopenssl + # webauthn django==3.2.20 # via # -r requirements/base.in @@ -75,11 +80,12 @@ django==3.2.20 # django-rosetta # django-simple-certmanager # django-solo + # django-two-factor-auth # djangorestframework # drf-nested-routers # drf-spectacular # drf-yasg - # maykin-django-two-factor-auth + # maykin-2fa # mozilla-django-oidc # mozilla-django-oidc-db # notifications-api-common @@ -94,7 +100,7 @@ django-filter==23.2 # -r requirements/base.in # commonground-api-common django-formtools==2.4.1 - # via maykin-django-two-factor-auth + # via django-two-factor-auth django-ipware==6.0.3 # via django-axes django-jsonform==2.21.5 @@ -104,9 +110,9 @@ django-jsonsuit==0.5.0 django-ordered-model==3.7.4 # via django-admin-index django-otp==1.2.0 - # via maykin-django-two-factor-auth + # via django-two-factor-auth django-phonenumber-field==5.2.0 - # via maykin-django-two-factor-auth + # via django-two-factor-auth django-privates==2.0.0.post0 # via django-simple-certmanager django-redis==5.2.0 @@ -128,6 +134,8 @@ django-solo==2.0.0 # notifications-api-common # sharing-configs # zgw-consumers +django-two-factor-auth[phonenumberslite,webauthn]==1.16.0 + # via maykin-2fa djangorestframework==3.12.4 # via # -r requirements/base.in @@ -185,7 +193,7 @@ kombu==5.2.4 # via celery markupsafe==2.1.2 # via jinja2 -maykin-django-two-factor-auth[phonenumbers]==2.0.4 +maykin-2fa==1.0.0 # via -r requirements/base.in mozilla-django-oidc==4.0.0 # via mozilla-django-oidc-db @@ -197,8 +205,8 @@ oyaml==1.0 # via commonground-api-common packaging==23.1 # via drf-yasg -phonenumbers==8.13.11 - # via maykin-django-two-factor-auth +phonenumberslite==8.13.30 + # via django-two-factor-auth pillow==9.5.0 # via -r requirements/base.in polib==1.2.0 @@ -213,10 +221,11 @@ pyjwt==2.7.0 # via # commonground-api-common # gemma-zds-client -pyopenssl==23.1.1 +pyopenssl==24.0.0 # via # django-simple-certmanager # josepy + # webauthn # zgw-consumers pyrsistent==0.19.3 # via jsonschema @@ -244,7 +253,7 @@ pyyaml==6.0 # gemma-zds-client # oyaml qrcode==6.1 - # via maykin-django-two-factor-auth + # via django-two-factor-auth redis==4.5.5 # via django-redis requests==2.31.0 @@ -291,6 +300,8 @@ vine==5.0.0 # kombu wcwidth==0.2.6 # via prompt-toolkit +webauthn==2.0.0 + # via django-two-factor-auth wrapt==1.15.0 # via elastic-apm zgw-consumers==0.26.1 diff --git a/requirements/ci.txt b/requirements/ci.txt index c47c56fa..0aceecbf 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -12,6 +12,10 @@ asgiref==3.6.0 # via # -r requirements/base.txt # django +asn1crypto==1.5.1 + # via + # -r requirements/base.txt + # webauthn async-timeout==4.0.2 # via # -r requirements/base.txt @@ -32,6 +36,10 @@ boltons==23.0.0 # -r requirements/base.txt # face # glom +cbor2==5.6.1 + # via + # -r requirements/base.txt + # webauthn celery==5.2.7 # via # -r requirements/base.txt @@ -82,13 +90,14 @@ coreschema==0.0.4 # coreapi coverage==4.5.4 # via -r requirements/test-tools.in -cryptography==40.0.2 +cryptography==42.0.2 # via # -r requirements/base.txt # django-simple-certmanager # josepy # mozilla-django-oidc # pyopenssl + # webauthn cssselect==1.2.0 # via pyquery django==3.2.20 @@ -112,11 +121,12 @@ django==3.2.20 # django-sendfile2 # django-simple-certmanager # django-solo + # django-two-factor-auth # djangorestframework # drf-nested-routers # drf-spectacular # drf-yasg - # maykin-django-two-factor-auth + # maykin-2fa # mozilla-django-oidc # mozilla-django-oidc-db # notifications-api-common @@ -133,7 +143,7 @@ django-filter==23.2 django-formtools==2.4.1 # via # -r requirements/base.txt - # maykin-django-two-factor-auth + # django-two-factor-auth django-ipware==6.0.3 # via # -r requirements/base.txt @@ -153,11 +163,11 @@ django-ordered-model==3.7.4 django-otp==1.2.0 # via # -r requirements/base.txt - # maykin-django-two-factor-auth + # django-two-factor-auth django-phonenumber-field==5.2.0 # via # -r requirements/base.txt - # maykin-django-two-factor-auth + # django-two-factor-auth django-privates==2.0.0.post0 # via # -r requirements/base.txt @@ -190,6 +200,10 @@ django-solo==2.0.0 # notifications-api-common # sharing-configs # zgw-consumers +django-two-factor-auth[phonenumberslite,webauthn]==1.16.0 + # via + # -r requirements/base.txt + # maykin-2fa django-webtest==1.9.10 # via -r requirements/test-tools.in djangorestframework==3.12.4 @@ -283,7 +297,7 @@ markupsafe==2.1.2 # via # -r requirements/base.txt # jinja2 -maykin-django-two-factor-auth[phonenumbers]==2.0.4 +maykin-2fa==1.0.0 # via -r requirements/base.txt mozilla-django-oidc==4.0.0 # via @@ -303,10 +317,10 @@ packaging==23.1 # via # -r requirements/base.txt # drf-yasg -phonenumbers==8.13.11 +phonenumberslite==8.13.30 # via # -r requirements/base.txt - # maykin-django-two-factor-auth + # django-two-factor-auth pillow==9.5.0 # via -r requirements/base.txt polib==1.2.0 @@ -328,11 +342,12 @@ pyjwt==2.7.0 # -r requirements/base.txt # commonground-api-common # gemma-zds-client -pyopenssl==23.1.1 +pyopenssl==24.0.0 # via # -r requirements/base.txt # django-simple-certmanager # josepy + # webauthn # zgw-consumers pyquery==2.0.0 # via -r requirements/test-tools.in @@ -371,7 +386,7 @@ pyyaml==6.0 qrcode==6.1 # via # -r requirements/base.txt - # maykin-django-two-factor-auth + # django-two-factor-auth redis==4.5.5 # via # -r requirements/base.txt @@ -438,6 +453,10 @@ wcwidth==0.2.6 # via # -r requirements/base.txt # prompt-toolkit +webauthn==2.0.0 + # via + # -r requirements/base.txt + # django-two-factor-auth webob==1.8.7 # via webtest webtest==3.0.0 diff --git a/requirements/dev.txt b/requirements/dev.txt index e08479af..82b0870f 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -14,6 +14,10 @@ asgiref==3.6.0 # via # -r requirements/ci.txt # django +asn1crypto==1.5.1 + # via + # -r requirements/ci.txt + # webauthn async-timeout==4.0.2 # via # -r requirements/ci.txt @@ -46,6 +50,10 @@ bump2version==1.0.1 # via bumpversion bumpversion==0.6.0 # via -r requirements/dev.in +cbor2==5.6.1 + # via + # -r requirements/ci.txt + # webauthn celery==5.2.7 # via # -r requirements/ci.txt @@ -98,13 +106,14 @@ coreschema==0.0.4 # coreapi coverage==4.5.4 # via -r requirements/ci.txt -cryptography==40.0.2 +cryptography==42.0.2 # via # -r requirements/ci.txt # django-simple-certmanager # josepy # mozilla-django-oidc # pyopenssl + # webauthn cssselect==1.2.0 # via # -r requirements/ci.txt @@ -132,11 +141,12 @@ django==3.2.20 # django-sendfile2 # django-simple-certmanager # django-solo + # django-two-factor-auth # djangorestframework # drf-nested-routers # drf-spectacular # drf-yasg - # maykin-django-two-factor-auth + # maykin-2fa # mozilla-django-oidc # mozilla-django-oidc-db # notifications-api-common @@ -157,7 +167,7 @@ django-filter==23.2 django-formtools==2.4.1 # via # -r requirements/ci.txt - # maykin-django-two-factor-auth + # django-two-factor-auth django-ipware==6.0.3 # via # -r requirements/ci.txt @@ -177,11 +187,11 @@ django-ordered-model==3.7.4 django-otp==1.2.0 # via # -r requirements/ci.txt - # maykin-django-two-factor-auth + # django-two-factor-auth django-phonenumber-field==5.2.0 # via # -r requirements/ci.txt - # maykin-django-two-factor-auth + # django-two-factor-auth django-privates==2.0.0.post0 # via # -r requirements/ci.txt @@ -214,6 +224,10 @@ django-solo==2.0.0 # notifications-api-common # sharing-configs # zgw-consumers +django-two-factor-auth[phonenumberslite,webauthn]==1.16.0 + # via + # -r requirements/ci.txt + # maykin-2fa django-webtest==1.9.10 # via -r requirements/ci.txt djangorestframework==3.12.4 @@ -320,7 +334,7 @@ markupsafe==2.1.2 # via # -r requirements/ci.txt # jinja2 -maykin-django-two-factor-auth[phonenumbers]==2.0.4 +maykin-2fa==1.0.0 # via -r requirements/ci.txt mccabe==0.7.0 # via flake8 @@ -349,10 +363,10 @@ packaging==23.1 # sphinx pathspec==0.11.1 # via black -phonenumbers==8.13.11 +phonenumberslite==8.13.30 # via # -r requirements/ci.txt - # maykin-django-two-factor-auth + # django-two-factor-auth pillow==9.5.0 # via -r requirements/ci.txt pip-tools==6.13.0 @@ -384,11 +398,12 @@ pyjwt==2.7.0 # -r requirements/ci.txt # commonground-api-common # gemma-zds-client -pyopenssl==23.1.1 +pyopenssl==24.0.0 # via # -r requirements/ci.txt # django-simple-certmanager # josepy + # webauthn # zgw-consumers pyproject-hooks==1.0.0 # via build @@ -429,7 +444,7 @@ pyyaml==6.0 qrcode==6.1 # via # -r requirements/ci.txt - # maykin-django-two-factor-auth + # django-two-factor-auth redis==4.5.5 # via # -r requirements/ci.txt @@ -528,6 +543,10 @@ wcwidth==0.2.6 # via # -r requirements/ci.txt # prompt-toolkit +webauthn==2.0.0 + # via + # -r requirements/ci.txt + # django-two-factor-auth webob==1.8.7 # via # -r requirements/ci.txt diff --git a/src/objecttypes/conf/base.py b/src/objecttypes/conf/base.py index 3a4443c3..e74b4586 100644 --- a/src/objecttypes/conf/base.py +++ b/src/objecttypes/conf/base.py @@ -75,11 +75,12 @@ "solo", "drf_spectacular", "vng_api_common", - # 2fa apps + # Two-factor authentication in the Django admin, enforced. "django_otp", "django_otp.plugins.otp_static", "django_otp.plugins.otp_totp", "two_factor", + "maykin_2fa", "sharing_configs", # Project applications. "objecttypes.accounts", @@ -96,11 +97,11 @@ "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", + "maykin_2fa.middleware.OTPMiddleware", "mozilla_django_oidc_db.middleware.SessionRefresh", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "axes.middleware.AxesMiddleware", - "django_otp.middleware.OTPMiddleware", ] ROOT_URLCONF = "objecttypes.urls" @@ -395,10 +396,18 @@ # -# Maykin fork of DJANGO-TWO-FACTOR-AUTH +# MAYKIN-2FA +# Uses django-two-factor-auth under the hood, so relevant upstream package settings +# apply too. # -TWO_FACTOR_FORCE_OTP_ADMIN = config("TWO_FACTOR_FORCE_OTP_ADMIN", not DEBUG) -TWO_FACTOR_PATCH_ADMIN = config("TWO_FACTOR_PATCH_ADMIN", True) + +# we run the admin site monkeypatch instead. +TWO_FACTOR_PATCH_ADMIN = False +# add entries from AUTHENTICATION_BACKENDS that already enforce their own two-factor +# auth, avoiding having some set up MFA again in the project. +MAYKIN_2FA_ALLOW_MFA_BYPASS_BACKENDS = [ + "mozilla_django_oidc_db.backends.OIDCAuthenticationBackend", +] # # Mozilla Django OIDC DB settings diff --git a/src/objecttypes/conf/ci.py b/src/objecttypes/conf/ci.py index 756a6fe1..e8c8c3ff 100644 --- a/src/objecttypes/conf/ci.py +++ b/src/objecttypes/conf/ci.py @@ -25,6 +25,3 @@ # Django-axes # AXES_BEHIND_REVERSE_PROXY = False - -# Maykin fork of DJANGO-TWO-FACTOR-AUTH -TWO_FACTOR_FORCE_OTP_ADMIN = False diff --git a/src/objecttypes/conf/dev.py b/src/objecttypes/conf/dev.py index c2e0000c..a467ddab 100644 --- a/src/objecttypes/conf/dev.py +++ b/src/objecttypes/conf/dev.py @@ -107,8 +107,9 @@ r"django\.db\.models\.fields", ) -if "test" in sys.argv: - TWO_FACTOR_FORCE_OTP_ADMIN = False +# None of the authentication backends require two-factor authentication. +if config("DISABLE_2FA", default=True): # pragma: no cover + MAYKIN_2FA_ALLOW_MFA_BYPASS_BACKENDS = AUTHENTICATION_BACKENDS # Override settings with local settings. try: diff --git a/src/objecttypes/templates/admin/base_site.html b/src/objecttypes/templates/admin/base_site.html index 45d313da..d8df6af0 100644 --- a/src/objecttypes/templates/admin/base_site.html +++ b/src/objecttypes/templates/admin/base_site.html @@ -23,9 +23,9 @@

{{ settings.PROJECT_NAME }} {% if site_url %} {{ settings.SITE_TITLE }} / {% endif %} - {% url 'admin:two_factor:profile' as 2fa_profile_url %} - {% if 2fa_profile_url %} - {% trans "View 2fa profile" %} / + {% url 'maykin_2fa:account_security' as 2fa_account_security_url %} + {% if 2fa_account_security_url %} + {% trans "Account security" %} / {% endif %} {% if user.has_usable_password %} {% trans 'Change password' %} / diff --git a/src/objecttypes/templates/admin/login.html b/src/objecttypes/templates/admin/login.html deleted file mode 100644 index d408af04..00000000 --- a/src/objecttypes/templates/admin/login.html +++ /dev/null @@ -1,16 +0,0 @@ -{% extends "two_factor/admin/login.html" %} -{% load solo_tags i18n %} - - -{% block content %} -{{ block.super }} - -{% get_solo 'mozilla_django_oidc_db.OpenIDConnectConfig' as oidc_config %} -{% if oidc_config.enabled %} -
{% trans "or" %}
-
- {% trans "Login with organization account" %} -
-{% endif %} - -{% endblock %} diff --git a/src/objecttypes/templates/maykin_2fa/base.html b/src/objecttypes/templates/maykin_2fa/base.html new file mode 100644 index 00000000..68fa4301 --- /dev/null +++ b/src/objecttypes/templates/maykin_2fa/base.html @@ -0,0 +1,9 @@ +{% extends "maykin_2fa/base.html" %} + +{# Django 3.2 #} +{% block breadcrumbs %}{% endblock %} + +{# Do not show any version information #} +{% block footer %} + +{% endblock %} diff --git a/src/objecttypes/templates/maykin_2fa/login.html b/src/objecttypes/templates/maykin_2fa/login.html new file mode 100644 index 00000000..51987a80 --- /dev/null +++ b/src/objecttypes/templates/maykin_2fa/login.html @@ -0,0 +1,23 @@ +{% extends "maykin_2fa/login.html" %} +{% load solo_tags i18n %} + +{% block extra_login_options %} + {% get_solo 'mozilla_django_oidc_db.OpenIDConnectConfig' as oidc_config %} + {% if oidc_config.enabled %} +
{% trans "or" %}
+
+ {% trans "Login with organization account" %} +
+ {% endif %} +{% endblock %} + +{% block extra_recovery_options %} +
  • + {% trans 'Contact support to start the account recovery process' %} +
  • +{% endblock extra_recovery_options %} + +{# Do not show any version information #} +{% block footer %} + +{% endblock %} diff --git a/src/objecttypes/templates/two_factor/admin/login.html b/src/objecttypes/templates/two_factor/admin/login.html deleted file mode 100644 index afdff9b3..00000000 --- a/src/objecttypes/templates/two_factor/admin/login.html +++ /dev/null @@ -1 +0,0 @@ -{% extends "admin/login.html" %} diff --git a/src/objecttypes/tests/test_objecttype_admin.py b/src/objecttypes/tests/test_objecttype_admin.py index f9fb9739..3d24f1f4 100644 --- a/src/objecttypes/tests/test_objecttype_admin.py +++ b/src/objecttypes/tests/test_objecttype_admin.py @@ -6,6 +6,7 @@ import requests_mock from django_webtest import WebTest from freezegun import freeze_time +from maykin_2fa.test import disable_admin_mfa from objecttypes.accounts.tests.factories import SuperUserFactory from objecttypes.core.constants import ( @@ -27,6 +28,7 @@ @freeze_time("2020-01-01") +@disable_admin_mfa() class AdminAddTests(WebTest): url = reverse_lazy("admin:core_objecttype_add") import_from_url = reverse_lazy("admin:import_from_url") @@ -244,6 +246,7 @@ def test_create_objecttype_from_url_with_nonexistent_url(self): self.assertEqual(ObjectType.objects.count(), 0) +@disable_admin_mfa() class AdminDetailTests(WebTest): @classmethod def setUpTestData(cls): diff --git a/src/objecttypes/tests/test_sharing_configs.py b/src/objecttypes/tests/test_sharing_configs.py index c88af9ce..e877f291 100644 --- a/src/objecttypes/tests/test_sharing_configs.py +++ b/src/objecttypes/tests/test_sharing_configs.py @@ -8,6 +8,7 @@ import requests_mock from django_webtest import WebTest from freezegun import freeze_time +from maykin_2fa.test import disable_admin_mfa from sharing_configs.models import SharingConfigsConfig from objecttypes.accounts.tests.factories import SuperUserFactory @@ -19,6 +20,7 @@ SHARING_CONFIGS_API_ROOT = "https://sharing-configs-api.example.org/api/v1/" +@disable_admin_mfa() @freeze_time("2020-01-01") class SharingConfigsTests(WebTest): def setUp(self) -> None: diff --git a/src/objecttypes/urls.py b/src/objecttypes/urls.py index b02f064d..a9c70b5f 100644 --- a/src/objecttypes/urls.py +++ b/src/objecttypes/urls.py @@ -7,6 +7,8 @@ from django.urls import include, path from django.views.generic.base import TemplateView +from maykin_2fa import monkeypatch_admin +from maykin_2fa.urls import urlpatterns as maykin_2fa_urlpatterns from rest_framework.settings import api_settings handler500 = "objecttypes.utils.views.server_error" @@ -14,6 +16,8 @@ admin.site.site_title = "objecttypes admin" admin.site.index_title = "Welcome to the objecttypes admin" +monkeypatch_admin() + urlpatterns = [ path( "admin/password_reset/", @@ -25,6 +29,7 @@ auth_views.PasswordResetDoneView.as_view(), name="password_reset_done", ), + path("admin/", include((maykin_2fa_urlpatterns, "maykin_2fa"))), path("admin/", admin.site.urls), path( "reset///",