diff --git a/docs/docs/start/config.md b/docs/docs/start/config.md index df314eeb0f..2dec53c44f 100644 --- a/docs/docs/start/config.md +++ b/docs/docs/start/config.md @@ -425,7 +425,7 @@ InvenTree provides allowance for additional sign-in options. The following optio | Environment Variable | Configuration File | Description | Default | | --- | --- | --- | --- | | INVENTREE_MFA_ENABLED | mfa_enabled | Enable or disable multi-factor authentication support for the InvenTree server | True | -| INVENTREE_MFA_SUPPORTED_TYPES | mfa_supported_types | List of supported multi-factor authentication types | recovery_codes,totp | +| INVENTREE_MFA_SUPPORTED_TYPES | mfa_supported_types | List of supported multi-factor authentication types | recovery_codes,totp,webauthn | ### Single Sign On diff --git a/src/backend/InvenTree/InvenTree/AllUserRequire2FAMiddleware.py b/src/backend/InvenTree/InvenTree/AllUserRequire2FAMiddleware.py deleted file mode 100644 index 160bd1ec9e..0000000000 --- a/src/backend/InvenTree/InvenTree/AllUserRequire2FAMiddleware.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Middleware to require 2FA for users.""" - -from django.http import HttpRequest, HttpResponse, JsonResponse -from django.utils.deprecation import MiddlewareMixin -from django.utils.translation import gettext_lazy as _ - -from allauth.account.authentication import get_authentication_records -from allauth.mfa.utils import is_mfa_enabled -from allauth.mfa.webauthn.internal.flows import did_use_passwordless_login - - -def is_multifactor_logged_in(request: HttpRequest) -> bool: - """Check if the user is logged in with multifactor authentication.""" - authns = get_authentication_records(request) - - return is_mfa_enabled(request.user) and ( - did_use_passwordless_login(request) - or any(record.get('method') == 'mfa' for record in authns) - ) - - -class AllUserRequire2FAMiddleware(MiddlewareMixin): - """Ensure that users have two-factor authentication enabled before they have access restricted endpoints. - - Adapted from https://github.com/pennersr/django-allauth/issues/3649 - """ - - allowed_pages = [ - 'api-user-meta', - 'api-user-me', - 'api-user-roles', - 'api-inventree-info', - 'api-token', - # web platform urls - 'password_reset_confirm', - 'web', - 'web-wildcard', - 'web-assets', - ] - app_names = ['headless'] - require_2fa_message = _( - 'You must enable two-factor authentication before doing anything else.' - ) - - def on_require_2fa(self, request: HttpRequest) -> HttpResponse: - """Force user to mfa activation.""" - return JsonResponse( - {'id': 'mfa_register', 'error': self.require_2fa_message}, status=401 - ) - - def is_allowed_page(self, request: HttpRequest) -> bool: - """Check if the current page can be accessed without mfa.""" - match = request.resolver_match - return ( - None - if match is None - else any(ref in self.app_names for ref in match.app_names) - or match.url_name in self.allowed_pages - or match.route == 'favicon.ico' - ) - - def enforce_2fa(self, request): - """Check if 2fa should be enforced for this request.""" - return True - - def process_view( - self, request: HttpRequest, view_func, view_args, view_kwargs - ) -> HttpResponse: - """If set up enforce 2fa registration.""" - if request.user.is_anonymous: - return None - if self.is_allowed_page(request): - return None - if is_multifactor_logged_in(request): - return None - - if self.enforce_2fa(request): - return self.on_require_2fa(request) - return None diff --git a/src/backend/InvenTree/InvenTree/middleware.py b/src/backend/InvenTree/InvenTree/middleware.py index c84ab0c89d..6b9e72d609 100644 --- a/src/backend/InvenTree/InvenTree/middleware.py +++ b/src/backend/InvenTree/InvenTree/middleware.py @@ -5,17 +5,17 @@ from urllib.parse import urlsplit from django.conf import settings from django.contrib.auth.middleware import PersistentRemoteUserMiddleware -from django.http import HttpResponse +from django.http import HttpRequest, HttpResponse, JsonResponse from django.shortcuts import redirect, render from django.urls import resolve, reverse_lazy from django.utils.deprecation import MiddlewareMixin from django.utils.http import is_same_domain +from django.utils.translation import gettext_lazy as _ import structlog from error_report.middleware import ExceptionProcessor from common.settings import get_global_setting -from InvenTree.AllUserRequire2FAMiddleware import AllUserRequire2FAMiddleware from InvenTree.cache import create_session_cache, delete_session_cache from InvenTree.config import CONFIG_LOOKUPS, inventreeInstaller from users.models import ApiToken @@ -146,8 +146,80 @@ class AuthRequiredMiddleware: return response -class Check2FAMiddleware(AllUserRequire2FAMiddleware): - """Ensure that mfa is enforced if set so.""" +class Check2FAMiddleware(MiddlewareMixin): + """Ensure that users have two-factor authentication enabled before they have access restricted endpoints. + + Adapted from https://github.com/pennersr/django-allauth/issues/3649 + """ + + allowed_pages = [ + 'api-user-meta', + 'api-user-me', + 'api-user-roles', + 'api-inventree-info', + 'api-token', + # web platform urls + 'password_reset_confirm', + 'index', + 'web', + 'web-wildcard', + 'web-assets', + ] + app_names = ['headless'] + require_2fa_message = _( + 'You must enable two-factor authentication before doing anything else.' + ) + + def on_require_2fa(self, request: HttpRequest) -> HttpResponse: + """Force user to mfa activation.""" + return JsonResponse( + {'id': 'mfa_register', 'error': self.require_2fa_message}, status=401 + ) + + def is_allowed_page(self, request: HttpRequest) -> bool: + """Check if the current page can be accessed without mfa.""" + match = request.resolver_match + return ( + None + if match is None + else any(ref in self.app_names for ref in match.app_names) + or match.url_name in self.allowed_pages + or match.route == 'favicon.ico' + ) + + def is_multifactor_logged_in(self, request: HttpRequest) -> bool: + """Check if the user is logged in with multifactor authentication.""" + from allauth.account.authentication import get_authentication_records + from allauth.mfa.utils import is_mfa_enabled + from allauth.mfa.webauthn.internal.flows import did_use_passwordless_login + + authns = get_authentication_records(request) + + return is_mfa_enabled(request.user) and ( + did_use_passwordless_login(request) + or any(record.get('method') == 'mfa' for record in authns) + ) + + def process_view( + self, request: HttpRequest, view_func, view_args, view_kwargs + ) -> HttpResponse: + """Determine if the server is set up enforce 2fa registration.""" + from django.conf import settings + + # Exit early if MFA is not enabled + if not settings.MFA_ENABLED: + return None + + if request.user.is_anonymous: + return None + if self.is_allowed_page(request): + return None + if self.is_multifactor_logged_in(request): + return None + + if self.enforce_2fa(request): + return self.on_require_2fa(request) + return None def enforce_2fa(self, request): """Use setting to check if MFA should be enforced.""" diff --git a/src/backend/InvenTree/InvenTree/settings.py b/src/backend/InvenTree/InvenTree/settings.py index 25f94b7c7e..4b5ec58951 100644 --- a/src/backend/InvenTree/InvenTree/settings.py +++ b/src/backend/InvenTree/InvenTree/settings.py @@ -1378,15 +1378,22 @@ ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = True HEADLESS_ONLY = True HEADLESS_CLIENTS = 'browser' -MFA_ENABLED = get_boolean_setting( - 'INVENTREE_MFA_ENABLED', 'mfa_enabled', True -) # TODO re-implement -MFA_SUPPORTED_TYPES = get_setting( - 'INVENTREE_MFA_SUPPORTED_TYPES', - 'mfa_supported_types', - ['totp', 'recovery_codes', 'webauthn'], - typecast=list, +MFA_ENABLED = get_boolean_setting('INVENTREE_MFA_ENABLED', 'mfa_enabled', True) + +if not MFA_ENABLED: + MIDDLEWARE.remove('InvenTree.middleware.Check2FAMiddleware') + +MFA_SUPPORTED_TYPES = ( + get_setting( + 'INVENTREE_MFA_SUPPORTED_TYPES', + 'mfa_supported_types', + ['totp', 'recovery_codes', 'webauthn'], + typecast=list, + ) + if MFA_ENABLED + else [] ) + MFA_TRUST_ENABLED = True MFA_PASSKEY_LOGIN_ENABLED = True MFA_WEBAUTHN_ALLOW_INSECURE_ORIGIN = DEBUG