mirror of
https://github.com/inventree/InvenTree.git
synced 2025-11-09 01:25:41 +00:00
MFA middleware tweaks (#10768)
* MFA middleware tweaks - Remove Check2FAMiddleware if MFA not enabled - Refactor into middleware.py * Update settings.py - Disable MFA_SUPPORTED_TYPES if MFA_ENABLED is False * Update docs
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user