mirror of
https://github.com/inventree/InvenTree.git
synced 2025-11-09 09:35:42 +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 |
|
| 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_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
|
### 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.conf import settings
|
||||||
from django.contrib.auth.middleware import PersistentRemoteUserMiddleware
|
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.shortcuts import redirect, render
|
||||||
from django.urls import resolve, reverse_lazy
|
from django.urls import resolve, reverse_lazy
|
||||||
from django.utils.deprecation import MiddlewareMixin
|
from django.utils.deprecation import MiddlewareMixin
|
||||||
from django.utils.http import is_same_domain
|
from django.utils.http import is_same_domain
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from error_report.middleware import ExceptionProcessor
|
from error_report.middleware import ExceptionProcessor
|
||||||
|
|
||||||
from common.settings import get_global_setting
|
from common.settings import get_global_setting
|
||||||
from InvenTree.AllUserRequire2FAMiddleware import AllUserRequire2FAMiddleware
|
|
||||||
from InvenTree.cache import create_session_cache, delete_session_cache
|
from InvenTree.cache import create_session_cache, delete_session_cache
|
||||||
from InvenTree.config import CONFIG_LOOKUPS, inventreeInstaller
|
from InvenTree.config import CONFIG_LOOKUPS, inventreeInstaller
|
||||||
from users.models import ApiToken
|
from users.models import ApiToken
|
||||||
@@ -146,8 +146,80 @@ class AuthRequiredMiddleware:
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class Check2FAMiddleware(AllUserRequire2FAMiddleware):
|
class Check2FAMiddleware(MiddlewareMixin):
|
||||||
"""Ensure that mfa is enforced if set so."""
|
"""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):
|
def enforce_2fa(self, request):
|
||||||
"""Use setting to check if MFA should be enforced."""
|
"""Use setting to check if MFA should be enforced."""
|
||||||
|
|||||||
@@ -1378,15 +1378,22 @@ ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = True
|
|||||||
|
|
||||||
HEADLESS_ONLY = True
|
HEADLESS_ONLY = True
|
||||||
HEADLESS_CLIENTS = 'browser'
|
HEADLESS_CLIENTS = 'browser'
|
||||||
MFA_ENABLED = get_boolean_setting(
|
MFA_ENABLED = get_boolean_setting('INVENTREE_MFA_ENABLED', 'mfa_enabled', True)
|
||||||
'INVENTREE_MFA_ENABLED', 'mfa_enabled', True
|
|
||||||
) # TODO re-implement
|
if not MFA_ENABLED:
|
||||||
MFA_SUPPORTED_TYPES = get_setting(
|
MIDDLEWARE.remove('InvenTree.middleware.Check2FAMiddleware')
|
||||||
|
|
||||||
|
MFA_SUPPORTED_TYPES = (
|
||||||
|
get_setting(
|
||||||
'INVENTREE_MFA_SUPPORTED_TYPES',
|
'INVENTREE_MFA_SUPPORTED_TYPES',
|
||||||
'mfa_supported_types',
|
'mfa_supported_types',
|
||||||
['totp', 'recovery_codes', 'webauthn'],
|
['totp', 'recovery_codes', 'webauthn'],
|
||||||
typecast=list,
|
typecast=list,
|
||||||
|
)
|
||||||
|
if MFA_ENABLED
|
||||||
|
else []
|
||||||
)
|
)
|
||||||
|
|
||||||
MFA_TRUST_ENABLED = True
|
MFA_TRUST_ENABLED = True
|
||||||
MFA_PASSKEY_LOGIN_ENABLED = True
|
MFA_PASSKEY_LOGIN_ENABLED = True
|
||||||
MFA_WEBAUTHN_ALLOW_INSECURE_ORIGIN = DEBUG
|
MFA_WEBAUTHN_ALLOW_INSECURE_ORIGIN = DEBUG
|
||||||
|
|||||||
Reference in New Issue
Block a user