From 315b5bd134e3942f4a5b446d8ee4a6ee90ec8ff9 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Tue, 3 Nov 2026 10:26:16 +0100 Subject: [PATCH] Merge commit from fork * remove mfa disableing on the token flow * also check for mfa in token issuance --- src/backend/InvenTree/InvenTree/middleware.py | 1 - .../InvenTree/InvenTree/test_middleware.py | 15 +++++++++++++++ src/backend/InvenTree/users/api.py | 19 ++++++++++++++++++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/middleware.py b/src/backend/InvenTree/InvenTree/middleware.py index 56afecdb3d..8834a5ebea 100644 --- a/src/backend/InvenTree/InvenTree/middleware.py +++ b/src/backend/InvenTree/InvenTree/middleware.py @@ -96,7 +96,6 @@ pages_mfa_bypass = [ 'api-user-me', 'api-user-roles', 'api-inventree-info', - 'api-token', # web platform urls 'password_reset_confirm', 'index', diff --git a/src/backend/InvenTree/InvenTree/test_middleware.py b/src/backend/InvenTree/InvenTree/test_middleware.py index 6cc99bdc47..04485d7261 100644 --- a/src/backend/InvenTree/InvenTree/test_middleware.py +++ b/src/backend/InvenTree/InvenTree/test_middleware.py @@ -89,6 +89,21 @@ class MiddlewareTests(InvenTreeTestCase): self.assertEqual(rsp3.status_code, 200) self.check_path(url) + def test_Check2FAMiddleware_token(self): + """Test that no API token can be generated before MFA is set up, if MFA is enforced.""" + url = reverse('api-token') + + with patch.dict('os.environ', {'INVENTREE_LOGIN_ENFORCE_MFA': 'True'}): + response = self.check_path(url, 401) + self.assertContains( + response, + 'You must enable two-factor authentication before doing anything else.', + status_code=401, + ) + + # Not enforced - token can be generated + self.check_path(url) + def test_token_auth(self): """Test auth with token auth.""" target = reverse('api-license') diff --git a/src/backend/InvenTree/users/api.py b/src/backend/InvenTree/users/api.py index 9b3a3cc0d0..2d8977021d 100644 --- a/src/backend/InvenTree/users/api.py +++ b/src/backend/InvenTree/users/api.py @@ -6,14 +6,20 @@ from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.db.models import Q from django.urls import include, path +from django.utils.translation import gettext_lazy as _ from django.views.decorators.csrf import ensure_csrf_cookie from django.views.generic.base import RedirectView import django_filters.rest_framework.filters as rest_filters import structlog +from allauth.account.internal.flows.reauthentication import ( + did_recently_authenticate, + get_reauthentication_flows, +) +from allauth.mfa.utils import is_mfa_enabled from django_filters.rest_framework.filterset import FilterSet from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema -from rest_framework import exceptions +from rest_framework import exceptions, status from rest_framework.generics import DestroyAPIView, GenericAPIView from rest_framework.response import Response @@ -404,6 +410,17 @@ class GetAuthToken(GenericAPIView): if not user.is_authenticated: raise exceptions.NotAuthenticated() # pragma: no cover + # does this user have MFA set up? if so: was he recently verified? + if is_mfa_enabled(user) and not did_recently_authenticate(request): + return Response( + { + 'id': 'reauthentication_required', + 'detail': _('Reauthentication is required to generate a token'), + 'data': {'flows': get_reauthentication_flows(user)}, + }, + status=status.HTTP_401_UNAUTHORIZED, + ) + name = ApiToken.sanitize_name(name) today = InvenTree.helpers.current_date()