mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +00:00 
			
		
		
		
	chore(backend):mfa e2e testing (#10206)
* chore(backend):mfa e2e testing * there are no tokens when only depoying browser-based clients for allauth * cleanup * style fixes
This commit is contained in:
		| @@ -12,13 +12,11 @@ import structlog | ||||
| from allauth.account.adapter import DefaultAccountAdapter | ||||
| from allauth.account.forms import LoginForm, SignupForm, set_form_field_order | ||||
| from allauth.headless.adapter import DefaultHeadlessAdapter | ||||
| from allauth.headless.tokens.sessions import SessionTokenStrategy | ||||
| from allauth.socialaccount.adapter import DefaultSocialAccountAdapter | ||||
|  | ||||
| import InvenTree.sso | ||||
| from common.settings import get_global_setting | ||||
| from InvenTree.exceptions import log_error | ||||
| from users.models import ApiToken | ||||
|  | ||||
| from .helpers_email import is_email_configured | ||||
|  | ||||
| @@ -231,17 +229,8 @@ class CustomHeadlessAdapter(DefaultHeadlessAdapter): | ||||
|         if urlname not in HEADLESS_FRONTEND_URLS: | ||||
|             raise ValueError( | ||||
|                 f'URL name "{urlname}" not found in HEADLESS_FRONTEND_URLS' | ||||
|             ) | ||||
|             )  # pragma: no cover | ||||
|  | ||||
|         return self.request.build_absolute_uri( | ||||
|             f'/{settings.FRONTEND_URL_BASE}/{HEADLESS_FRONTEND_URLS[urlname].format(**kwargs)}' | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class DRFTokenStrategy(SessionTokenStrategy): | ||||
|     """Strategy that InvenTrees own included Token model.""" | ||||
|  | ||||
|     def create_access_token(self, request): | ||||
|         """Create a new access token for the user.""" | ||||
|         token, _ = ApiToken.objects.get_or_create(user=request.user) | ||||
|         return token.key | ||||
|   | ||||
| @@ -1356,7 +1356,6 @@ HEADLESS_ADAPTER = 'InvenTree.auth_overrides.CustomHeadlessAdapter' | ||||
| ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = True | ||||
|  | ||||
| HEADLESS_ONLY = True | ||||
| HEADLESS_TOKEN_STRATEGY = 'InvenTree.auth_overrides.DRFTokenStrategy' | ||||
| HEADLESS_CLIENTS = 'browser' | ||||
| MFA_ENABLED = get_boolean_setting( | ||||
|     'INVENTREE_MFA_ENABLED', 'mfa_enabled', True | ||||
|   | ||||
| @@ -5,6 +5,8 @@ from django.contrib.auth.models import Group | ||||
| from django.test import TestCase | ||||
| from django.urls import reverse | ||||
|  | ||||
| from allauth.mfa.totp.internal import auth as totp_auth | ||||
|  | ||||
| from common.settings import set_global_setting | ||||
| from InvenTree.unit_test import AdminTestCase, InvenTreeAPITestCase, InvenTreeTestCase | ||||
| from users.models import ApiToken, Owner | ||||
| @@ -332,47 +334,83 @@ class OwnerModelTest(InvenTreeTestCase): | ||||
| class MFALoginTest(InvenTreeAPITestCase): | ||||
|     """Some simplistic tests to ensure that MFA is working.""" | ||||
|  | ||||
|     """ | ||||
|     def test_api(self): | ||||
|         ""Test that the API is working."" | ||||
|         auth_data = {'username': self.username, 'password': self.password} | ||||
|         login_url = reverse('api-login') | ||||
|     mfa_secret = None | ||||
|  | ||||
|         # Normal login | ||||
|         response = self.post(login_url, auth_data, expected_code=200) | ||||
|         self.assertIn('key', response.data) | ||||
|     def test_api(self): | ||||
|         """Test that the API is working.""" | ||||
|         auth_data = {'username': self.username, 'password': self.password} | ||||
|         login_url = reverse('browser:account:login') | ||||
|  | ||||
|         # Double login is not allowed | ||||
|         self.post(login_url, auth_data, expected_code=409) | ||||
|  | ||||
|         # Normal login - no mfa | ||||
|         self.client.logout() | ||||
|         response = self.post(login_url, auth_data, expected_code=200) | ||||
|         self._helper_meta_val(response) | ||||
|  | ||||
|         # Add MFA | ||||
|         totp_model = self.user.totpdevice_set.create() | ||||
|         response = self.post( | ||||
|             reverse('browser:mfa:manage_totp'), | ||||
|             {'code': self.get_topt()}, | ||||
|             expected_code=200, | ||||
|         ) | ||||
|         # There must be a TOTP device now - success | ||||
|         self.get(reverse('browser:mfa:manage_totp'), expected_code=200) | ||||
|         self.get(reverse('api-token'), expected_code=200) | ||||
|  | ||||
|         # Login with MFA enabled but not provided | ||||
|         response = self.post(login_url, auth_data, expected_code=403) | ||||
|         self.assertContains(response, 'MFA required for this user', status_code=403) | ||||
|  | ||||
|         # Login with MFA enabled and provided - should redirect to MFA page | ||||
|         auth_data['mfa'] = 'anything' | ||||
|         response = self.post(login_url, auth_data, expected_code=302) | ||||
|         self.assertEqual(response.url, reverse('two-factor-authenticate')) | ||||
|         # MFA not finished - no access allowed | ||||
|         self.client.logout() | ||||
|         response = self.post(login_url, auth_data, expected_code=401) | ||||
|         self._helper_meta_val(response, val=False) | ||||
|         self.assertEqual(self._helper_get_flow(response)['is_pending'], True) | ||||
|         self.get(reverse('api-token'), expected_code=401) | ||||
|  | ||||
|         # Login with MFA enabled and provided - second api call an success | ||||
|         self.client.logout() | ||||
|         response = self.post(login_url, auth_data, expected_code=401) | ||||
|         # MFA not finished - no access allowed | ||||
|         self.get(reverse('api-token'), expected_code=401) | ||||
|         # Complete | ||||
|         self.post( | ||||
|             reverse('browser:mfa:authenticate'), | ||||
|             {'code': self.get_topt()}, | ||||
|             expected_code=401, | ||||
|         ) | ||||
|         self.post(reverse('browser:mfa:trust'), {'trust': False}, expected_code=200) | ||||
|         # and run through trust | ||||
|         self.get(reverse('api-token'), expected_code=200) | ||||
|  | ||||
|         # Login with MFA enabled and provided - but incorrect pwd | ||||
|         self.client.logout() | ||||
|         auth_data['password'] = 'wrong' | ||||
|         self.post(login_url, auth_data, expected_code=401) | ||||
|         response = self.post(login_url, auth_data, expected_code=400) | ||||
|         self.assertContains( | ||||
|             response, | ||||
|             'The username and/or password you specified are not correct', | ||||
|             status_code=400, | ||||
|         ) | ||||
|         auth_data['password'] = self.password | ||||
|  | ||||
|         # Remove MFA | ||||
|         totp_model.delete() | ||||
|     def _helper_meta_val( | ||||
|         self, response, key: str = 'is_authenticated', val: bool = True | ||||
|     ): | ||||
|         """Helper to run a test on meta response.""" | ||||
|         self.assertEqual(response.json()['meta'][key], val) | ||||
|  | ||||
|         # Login with MFA disabled but correct credentials provided | ||||
|         response = self.post(login_url, auth_data, expected_code=200) | ||||
|         self.assertIn('key', response.data) | ||||
|     def _helper_get_flow(self, response, flow_id: str = 'mfa_authenticate'): | ||||
|         """Helper to run a test on flow response.""" | ||||
|         flows = response.json()['data']['flows'] | ||||
|         return next(a for a in flows if a['id'] == flow_id) | ||||
|  | ||||
|         # Wrong login should not work | ||||
|         auth_data['password'] = 'wrong' | ||||
|         self.post(login_url, auth_data, expected_code=401) | ||||
|     """ | ||||
|     def get_topt(self): | ||||
|         """Helper to get a current totp code.""" | ||||
|         if not self.mfa_secret: | ||||
|             mfa_init = self.get(reverse('browser:mfa:manage_totp'), expected_code=404) | ||||
|             self.mfa_secret = mfa_init.json()['meta']['secret'] | ||||
|         return totp_auth.hotp_value( | ||||
|             self.mfa_secret, next(totp_auth.yield_hotp_counters_from_time()) | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class AdminTest(AdminTestCase): | ||||
|   | ||||
		Reference in New Issue
	
	Block a user