From f8571a0a55d7757e33c5918318269d0e88071326 Mon Sep 17 00:00:00 2001 From: Augustin-FL Date: Wed, 28 Aug 2024 21:58:49 +0200 Subject: [PATCH] Move 2FA auth to a dedicated module --- docker/fir.env | 2 - fir/config/base.py | 42 +-------- fir/decorators.py | 12 +-- fir/urls.py | 17 +--- fir_auth_2fa/README.md | 34 +++++++ fir_auth_2fa/__init__.py | 1 + fir_auth_2fa/decorator.py | 28 ++++++ fir_auth_2fa/forms.py | 9 ++ fir_auth_2fa/requirements.txt | 3 + .../plugins/user_profile_actions.html | 2 + .../templates/two_factor/_base.html | 2 +- .../two_factor/core/setup_complete.html | 24 +++++ .../templates/two_factor/login.html | 0 fir_auth_2fa/urls.py | 30 +++++++ fir_auth_2fa/views.py | 90 +++++++++++++++++++ incidents/forms.py | 15 +--- incidents/views.py | 83 +---------------- requirements.txt | 2 - 18 files changed, 236 insertions(+), 160 deletions(-) create mode 100644 fir_auth_2fa/README.md create mode 100644 fir_auth_2fa/__init__.py create mode 100644 fir_auth_2fa/decorator.py create mode 100644 fir_auth_2fa/forms.py create mode 100644 fir_auth_2fa/requirements.txt create mode 100644 fir_auth_2fa/templates/fir_auth_2fa/plugins/user_profile_actions.html rename {incidents => fir_auth_2fa}/templates/two_factor/_base.html (96%) create mode 100644 fir_auth_2fa/templates/two_factor/core/setup_complete.html rename {incidents => fir_auth_2fa}/templates/two_factor/login.html (100%) create mode 100644 fir_auth_2fa/urls.py create mode 100644 fir_auth_2fa/views.py diff --git a/docker/fir.env b/docker/fir.env index e7de0e59..539d5659 100644 --- a/docker/fir.env +++ b/docker/fir.env @@ -19,7 +19,5 @@ REDIS_DB=0 HTTPS=false -ENFORCE_2FA=false - EMAIL_HOST=fir_fake_smtp EMAIL_PORT=1025 diff --git a/fir/config/base.py b/fir/config/base.py index da6c9ad1..7bb8c7f5 100755 --- a/fir/config/base.py +++ b/fir/config/base.py @@ -11,28 +11,8 @@ # Django settings for fir project. - -ENFORCE_2FA = bool(strtobool(os.getenv('ENFORCE_2FA', 'False'))) - -tf_error_message = """Django two factor is not installed and ENFORCE_2FA is set to True. -Either set ENFORCE_2FA to False or pip install django-two-factor-auth -""" - -try: - import two_factor - TF_INSTALLED = True -except ImportError: - if ENFORCE_2FA: - raise RuntimeWarning(tf_error_message) - TF_INSTALLED = False - - -if TF_INSTALLED: - LOGIN_URL = 'two_factor:login' - LOGIN_REDIRECT_URL = 'two_factor:profile' -else: - LOGIN_URL = "/login/" - LOGOUT_URL = "/logout/" +LOGIN_URL = "/login/" +LOGOUT_URL = "/logout/" # Local time zone for this installation. Choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name @@ -85,10 +65,6 @@ # 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) -if TF_INSTALLED: - TF_MIDDLEWARE = ('django_otp.middleware.OTPMiddleware',) - MIDDLEWARE = MIDDLEWARE + TF_MIDDLEWARE - # Authentication and authorization backends AUTHENTICATION_BACKENDS = ( @@ -128,20 +104,6 @@ 'colorfield' ) -if TF_INSTALLED: - TF_APPS = ( - 'django_otp', - 'django_otp.plugins.otp_static', - 'django_otp.plugins.otp_totp', - 'two_factor' - ) - INSTALLED_APPS = INSTALLED_APPS + TF_APPS - try: - import otp_yubikey - INSTALLED_APPS = INSTALLED_APPS + ('otp_yubikey', 'two_factor.plugins.yubikey') - except ImportError: - pass - apps_file = os.path.join(BASE_DIR, 'fir', 'config', 'installed_apps.txt') if os.path.exists(apps_file): apps = list(INSTALLED_APPS) diff --git a/fir/decorators.py b/fir/decorators.py index c17629c0..ad0ee36c 100644 --- a/fir/decorators.py +++ b/fir/decorators.py @@ -3,17 +3,11 @@ from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth.decorators import login_required -from fir.config.base import TF_INSTALLED, ENFORCE_2FA def fir_auth_required(view=None, redirect_field_name=None, login_url=None): - if TF_INSTALLED: - from django_otp.decorators import otp_required - if ENFORCE_2FA: - decorator = otp_required(view=view, redirect_field_name=REDIRECT_FIELD_NAME, login_url=login_url, if_configured=False) - else: - decorator = otp_required(view=view, redirect_field_name=REDIRECT_FIELD_NAME, login_url=login_url, if_configured=True) - else: - decorator = login_required(function=view, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None) + decorator = login_required( + function=view, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None + ) return decorator diff --git a/fir/urls.py b/fir/urls.py index d01a2f1f..1cea418c 100755 --- a/fir/urls.py +++ b/fir/urls.py @@ -2,7 +2,7 @@ from django.urls import include, re_path from django.contrib import admin -from fir.config.base import INSTALLED_APPS, TF_INSTALLED +from fir.config.base import INSTALLED_APPS from incidents import views @@ -18,22 +18,9 @@ re_path(r'^dashboard/', include(('incidents.custom_urls.dashboard', 'dashboard'), namespace='dashboard')), re_path(r'^admin/', admin.site.urls), re_path(r'^$', views.dashboard_main), + re_path(r'^login/', views.user_login, name='login') ] -if TF_INSTALLED: - from two_factor.views import LoginView - from two_factor.urls import urlpatterns as tf_urls - custom_urls = [] - for tf_url in tf_urls[0]: - if tf_url.name != "login": - custom_urls.append(tf_url) - custom_urls.append(re_path(r'^account/login/$', - view=views.CustomLoginView.as_view(), - name='login',)) - urlpatterns.append(re_path(r'', include((custom_urls, 'two_factor')))) -else: - urlpatterns.append(re_path(r'^login/', views.user_login, name='login')) - for app in INSTALLED_APPS: if app.startswith('fir_'): diff --git a/fir_auth_2fa/README.md b/fir_auth_2fa/README.md new file mode 100644 index 00000000..9df62815 --- /dev/null +++ b/fir_auth_2fa/README.md @@ -0,0 +1,34 @@ +This module allows users to use 2FA when connecting to FIR. + +The module supports the following second-factors: +- Yubikeys, which are validated against YubiCloud by default +- Webauthn +- TOTP + +## Install + +Follow the generic plugin installation instructions in [the FIR wiki](https://github.com/certsocietegenerale/FIR/wiki/Plugins). + +Once installed, please set the following settings in `production.py` + +``` +ENFORCE_2FA = True # If False, 2FA will be enabled but not enforced + +LOGIN_URL = "two_factor:login" +LOGIN_REDIRECT_URL = "two_factor:profile" +MIDDLEWARE += ( + "django_otp.middleware.OTPMiddleware", +) +INSTALLED_APPS += ( + "django_otp", + "django_otp.plugins.otp_static", + "django_otp.plugins.otp_totp", + "two_factor", + "otp_yubikey", + "two_factor.plugins.yubikey", # <- for yubikey capability + "two_factor.plugins.webauthn", # <- for webauthn capability +) + +# Webauthn Relying Party +TWO_FACTOR_WEBAUTHN_RP_NAME = 'YOURFIRINSTALL' +``` diff --git a/fir_auth_2fa/__init__.py b/fir_auth_2fa/__init__.py new file mode 100644 index 00000000..c90bc539 --- /dev/null +++ b/fir_auth_2fa/__init__.py @@ -0,0 +1 @@ +import fir_auth_2fa.decorator diff --git a/fir_auth_2fa/decorator.py b/fir_auth_2fa/decorator.py new file mode 100644 index 00000000..b9c6e0e2 --- /dev/null +++ b/fir_auth_2fa/decorator.py @@ -0,0 +1,28 @@ +from django_otp.decorators import otp_required +from django.conf import settings +from django.contrib.auth import REDIRECT_FIELD_NAME + +import fir.decorators + + +def fir_auth_required_2fa(view=None, redirect_field_name=None, login_url=None): + if hasattr(settings, "ENFORCE_2FA") and settings.ENFORCE_2FA: + decorator = otp_required( + view=view, + redirect_field_name=REDIRECT_FIELD_NAME, + login_url=login_url, + if_configured=False, + ) + else: + decorator = otp_required( + view=view, + redirect_field_name=REDIRECT_FIELD_NAME, + login_url=login_url, + if_configured=True, + ) + + return decorator + + +# Patch decorator to enable 2FA +fir.decorators.fir_auth_required = fir_auth_required_2fa diff --git a/fir_auth_2fa/forms.py b/fir_auth_2fa/forms.py new file mode 100644 index 00000000..5050a3e7 --- /dev/null +++ b/fir_auth_2fa/forms.py @@ -0,0 +1,9 @@ +from two_factor.forms import AuthenticationTokenForm + + +class CustomAuthenticationTokenForm(AuthenticationTokenForm): + def __init__(self, user, initial_device, **kwargs): + super(CustomAuthenticationTokenForm, self).__init__( + user, initial_device, **kwargs + ) + self.fields["otp_token"].widget.attrs.update({"class": "form-control"}) diff --git a/fir_auth_2fa/requirements.txt b/fir_auth_2fa/requirements.txt new file mode 100644 index 00000000..6386a6f4 --- /dev/null +++ b/fir_auth_2fa/requirements.txt @@ -0,0 +1,3 @@ +django-otp-yubikey +django-two-factor-auth[phonenumbers] +django-two-factor-auth[webauthn] diff --git a/fir_auth_2fa/templates/fir_auth_2fa/plugins/user_profile_actions.html b/fir_auth_2fa/templates/fir_auth_2fa/plugins/user_profile_actions.html new file mode 100644 index 00000000..ef916b59 --- /dev/null +++ b/fir_auth_2fa/templates/fir_auth_2fa/plugins/user_profile_actions.html @@ -0,0 +1,2 @@ +{% load i18n %} +
  • {% trans "Manage 2FA" %}
  • diff --git a/incidents/templates/two_factor/_base.html b/fir_auth_2fa/templates/two_factor/_base.html similarity index 96% rename from incidents/templates/two_factor/_base.html rename to fir_auth_2fa/templates/two_factor/_base.html index 07d875a4..b316e45f 100644 --- a/incidents/templates/two_factor/_base.html +++ b/fir_auth_2fa/templates/two_factor/_base.html @@ -12,7 +12,7 @@ - + {% block extra_media %}{% endblock %}