mirror of
https://github.com/inventree/InvenTree.git
synced 2026-11-10 10:57:52 +00:00
feat(backend): enable disabling of basic authentication (#13169)
* !feat(backend): disable basic authentication by default * align PR * add PR number * fix schema creation/discovery * mask basic auth * fix error code * reduce diff * adjust changelog message * fix test (needs basic auth which is now off by default) * switch to default on; retain warning * update test for new default * fix assertation
This commit is contained in:
1 parent
be9639266d
commit
a8a867400d
15 files changed
+90
-5
No files matched your search
@@ -321,6 +321,7 @@ jobs:
|
||||
INVENTREE_PYTHON_TEST_SERVER: http://127.0.0.1:12345
|
||||
INVENTREE_PYTHON_TEST_USERNAME: testuser
|
||||
INVENTREE_PYTHON_TEST_PASSWORD: testpassword
|
||||
INVENTREE_BASIC_AUTH_ENABLED: true
|
||||
INVENTREE_SITE_URL: http://127.0.0.1:12345
|
||||
INVENTREE_DEBUG: true
|
||||
INVENTREE_LOG_LEVEL: WARNING
|
||||
|
||||
@@ -49,6 +49,10 @@ Token authentication is recommended as it provides much faster API access and is
|
||||
|
||||
Users can authenticate against the API using basic authentication - specifically a valid combination of `username` and `password` credentials.
|
||||
|
||||
!!! warning "Can be disabled"
|
||||
Basic authentication can be disabled. This is controlled by the setting `INVENTREE_BASIC_AUTH_ENABLED` (or `basic_auth_enabled` in the [configuration file](../start/config.md#authentication)) to `False`. A warning ([INVE-W18](../settings/error_codes.md#inve-w18)) is logged and shown to staff users while it is enabled.
|
||||
|
||||
|
||||
Basic authentication attempts might run into rate limits during authentication on busy instances, as this is a likely place of attacks. Prefer Token or OAuth2 authentication instead.
|
||||
|
||||
!!! warning "Multi factor authentication"
|
||||
@@ -63,7 +67,7 @@ Each user is assigned an authentication token which can be used to access the AP
|
||||
|
||||
#### Requesting a Token
|
||||
|
||||
If a user does not already have an access token, they can request one via the user interface under the security user settings or the API interface, using a basic authentication request.
|
||||
If a user does not already have an access token, they can request one via the user interface under the security user settings or the API interface, using a basic authentication request (only if [basic authentication](#basic-auth) is enabled).
|
||||
|
||||
!!! warning "Tokens are only available once"
|
||||
Regardless of the request path used to obtain the token, it will only be displayed or provided once. Ensure that you copy and store it securely when it is first issued. Requesting a token with the same name will re-issue a new token, invalidating all previous ones of the same name.
|
||||
|
||||
@@ -262,6 +262,13 @@ The `middleware` list provided in the configuration file does not include the au
|
||||
|
||||
The middleware has been added automatically. To remove this warning, add it to the `middleware` list in your configuration file (after `django_structlog.middlewares.RequestMiddleware`).
|
||||
|
||||
#### INVE-W18
|
||||
**Basic authentication is enabled - Backend**
|
||||
|
||||
Basic (username:password) authentication against the API is enabled via the `INVENTREE_BASIC_AUTH_ENABLED` / `basic_auth_enabled` setting. Sending the user password with every request increases the impact of leaked credentials and makes the API a simpler target for brute-force attacks. Basic authentication also bypasses the interactive login flow and does not work for users with multi-factor authentication enabled.
|
||||
|
||||
It is recommended to disable basic authentication and use [Token or OAuth2 authentication](../api/index.md#authentication) instead - see the [Authentication settings](../start/config.md#authentication).
|
||||
|
||||
#### INVE-W20
|
||||
**Password authentication not allowed due to MFA - Backend**
|
||||
|
||||
|
||||
@@ -453,6 +453,7 @@ InvenTree provides allowance for additional sign-in options. The following optio
|
||||
{{ configsetting("INVENTREE_MFA_ENABLED") }} Enable multi-factor authentication support for the InvenTree server |
|
||||
{{ configsetting("INVENTREE_MFA_SUPPORTED_TYPES") }} List of supported multi-factor authentication types |
|
||||
{{ configsetting("INVENTREE_USE_JWT") }} Enable support for JSON Web Tokens (JWT) for authentication |
|
||||
{{ configsetting("INVENTREE_BASIC_AUTH_ENABLED") }} Enable/Disable basic (username:password) authentication against the API. See [Basic Auth](../api/index.md#basic-auth) |
|
||||
|
||||
### Single Sign On
|
||||
|
||||
|
||||
@@ -355,7 +355,10 @@ class InfoView(APIView):
|
||||
'LOGIN_ENABLE_PWD_FORGOT'
|
||||
),
|
||||
},
|
||||
'system_state': {'cors_allow_all': settings.CORS_ALLOW_ALL_ORIGINS},
|
||||
'system_state': {
|
||||
'cors_allow_all': settings.CORS_ALLOW_ALL_ORIGINS,
|
||||
'basic_auth_enabled': settings.BASIC_AUTH_ENABLED if is_staff else None,
|
||||
},
|
||||
}
|
||||
if nonce := request.query_params.get('link_nonce'):
|
||||
data['link'] = calculate_proof(nonce)
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 573
|
||||
INVENTREE_API_VERSION = 574
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v574 -> 2026-11-07 : https://github.com/inventree/InvenTree/pull/13169
|
||||
- breaking: Basic authentication can now be disabled - controlled with INVENTREE_BASIC_AUTH_ENABLED
|
||||
|
||||
v573 -> 2026-11-07 : https://github.com/inventree/InvenTree/pull/13221
|
||||
- Mark audit log changes_detail field as nullable
|
||||
- Add warning field to ApiToken
|
||||
|
||||
@@ -585,6 +585,15 @@ TEMPLATES = [
|
||||
}
|
||||
]
|
||||
|
||||
# Basic (username:password) authentication against the API - disabled by default
|
||||
BASIC_AUTH_ENABLED = get_boolean_setting(
|
||||
'INVENTREE_BASIC_AUTH_ENABLED', 'basic_auth_enabled', True
|
||||
)
|
||||
if BASIC_AUTH_ENABLED:
|
||||
logger.warning(
|
||||
'INVE-W18: Basic (username:password) authentication is enabled for the API - this is not recommended'
|
||||
)
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'EXCEPTION_HANDLER': 'InvenTree.exceptions.exception_handler',
|
||||
'DATETIME_FORMAT': '%Y-%m-%d %H:%M',
|
||||
|
||||
@@ -273,6 +273,7 @@ class OAuth2ApplicationAPITests(InvenTreeAPITestCase):
|
||||
)
|
||||
|
||||
|
||||
@override_settings(BASIC_AUTH_ENABLED=True)
|
||||
class ApiAccessTests(InvenTreeAPITestCase):
|
||||
"""Tests for various access scenarios with the InvenTree API."""
|
||||
|
||||
@@ -310,6 +311,14 @@ class ApiAccessTests(InvenTreeAPITestCase):
|
||||
self.get(url, expected_code=401)
|
||||
self.assertIsNone(self.token)
|
||||
|
||||
@override_settings(BASIC_AUTH_ENABLED=False)
|
||||
def test_basic_auth_disabled(self):
|
||||
"""Test that basic auth is rejected if it is disabled."""
|
||||
self.basicAuth()
|
||||
response = self.get(reverse('api-token'), expected_code=401)
|
||||
self.assertNotIn('Basic', response.get('WWW-Authenticate', ''))
|
||||
self.get(reverse('api-user-roles'), expected_code=401)
|
||||
|
||||
def test_token_success(self):
|
||||
"""Test token auth works."""
|
||||
self.tokenAuth()
|
||||
@@ -875,6 +884,11 @@ class GeneralApiTests(InvenTreeAPITestCase):
|
||||
self.assertIn('instance', data)
|
||||
|
||||
self.assertEqual('InvenTree', data['server'])
|
||||
self.assertTrue(data['system_state']['basic_auth_enabled'])
|
||||
|
||||
with override_settings(BASIC_AUTH_ENABLED=False):
|
||||
data = self.get(url, max_query_count=20).json()
|
||||
self.assertFalse(data['system_state']['basic_auth_enabled'])
|
||||
|
||||
# Test with token
|
||||
token = self.get(url=reverse('api-token'), max_query_count=40).data['token']
|
||||
|
||||
@@ -482,6 +482,7 @@ class AuditAPITest(AuditTestMixin, InvenTreeAPITestCase):
|
||||
self.assertTrue(all(cids))
|
||||
self.assertNotEqual(cids[0], cids[1])
|
||||
|
||||
@override_settings(BASIC_AUTH_ENABLED=True)
|
||||
def test_basic_auth_actor(self):
|
||||
"""Users authenticated by DRF (not middleware) are recorded as the actor."""
|
||||
self.logout()
|
||||
|
||||
@@ -2545,7 +2545,8 @@ class OrderCalendarExport(ICalFeed):
|
||||
response = JsonResponse({
|
||||
'detail': 'Authentication credentials were not provided.'
|
||||
})
|
||||
response['WWW-Authenticate'] = 'Basic realm="api"'
|
||||
if settings.BASIC_AUTH_ENABLED:
|
||||
response['WWW-Authenticate'] = 'Basic realm="api"'
|
||||
response.status_code = 401
|
||||
return response
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import connection
|
||||
from django.test import TransactionTestCase, skipUnlessDBFeature
|
||||
from django.test import TransactionTestCase, override_settings, skipUnlessDBFeature
|
||||
from django.test.utils import CaptureQueriesContext
|
||||
from django.urls import reverse
|
||||
|
||||
@@ -995,6 +995,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
resp_dict['detail'], 'Authentication credentials were not provided.'
|
||||
)
|
||||
|
||||
@override_settings(BASIC_AUTH_ENABLED=True)
|
||||
def test_po_calendar_auth(self):
|
||||
"""Test accessing calendar with header authorization."""
|
||||
self.client.logout()
|
||||
@@ -1008,6 +1009,22 @@ class PurchaseOrderTest(OrderTest):
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@override_settings(BASIC_AUTH_ENABLED=False)
|
||||
def test_po_calendar_auth_disabled(self):
|
||||
"""Test that header authorization is rejected if basic auth is disabled."""
|
||||
self.client.logout()
|
||||
base64_token = base64.b64encode(
|
||||
f'{self.username}:{self.password}'.encode('ascii')
|
||||
).decode('ascii')
|
||||
response = self.client.get(
|
||||
reverse('api-po-so-calendar', kwargs={'ordertype': 'purchase-order'}),
|
||||
format='json',
|
||||
headers={'authorization': f'basic {base64_token}'},
|
||||
)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
self.assertNotIn('WWW-Authenticate', response)
|
||||
|
||||
@override_settings(BASIC_AUTH_ENABLED=True)
|
||||
def test_po_calendar_auth_password_colon(self):
|
||||
"""Test header authorization with a password containing a colon."""
|
||||
password = 'pass:word:123'
|
||||
@@ -1025,6 +1042,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@override_settings(BASIC_AUTH_ENABLED=True)
|
||||
def test_po_calendar_auth_malformed(self):
|
||||
"""Test that malformed basic authorization headers return 401, not 500."""
|
||||
self.client.logout()
|
||||
@@ -1041,6 +1059,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
self.assertEqual(response.status_code, 401)
|
||||
self.assertEqual(response['WWW-Authenticate'], 'Basic realm="api"')
|
||||
|
||||
@override_settings(BASIC_AUTH_ENABLED=True)
|
||||
def test_po_calendar_auth_mfa(self):
|
||||
"""Test that header authorization creates no session, and is refused for users with MFA."""
|
||||
self.client.logout()
|
||||
|
||||
@@ -33,8 +33,21 @@ class InvenTreeBasicAuthentication(BasicAuthentication):
|
||||
|
||||
Checks:
|
||||
- If MFA is required and disallows basic auth
|
||||
- Only works if BASIC_AUTH_ENABLED is True
|
||||
"""
|
||||
|
||||
def authenticate(self, request):
|
||||
"""Only attempt basic authentication if it is enabled."""
|
||||
if not settings.BASIC_AUTH_ENABLED:
|
||||
return None
|
||||
return super().authenticate(request)
|
||||
|
||||
def authenticate_header(self, request):
|
||||
"""Only advertise basic authentication if it is enabled."""
|
||||
if not settings.BASIC_AUTH_ENABLED:
|
||||
return ''
|
||||
return super().authenticate_header(request)
|
||||
|
||||
def authenticate_credentials(self, userid, password, request=None):
|
||||
"""Also check if MFA is required for the user."""
|
||||
user, auth = super().authenticate_credentials(userid, password, request)
|
||||
|
||||
@@ -127,6 +127,7 @@ export default defineConfig({
|
||||
INVENTREE_SITE_URL: 'http://localhost:8000',
|
||||
INVENTREE_FRONTEND_API_HOST: 'http://localhost:8000',
|
||||
INVENTREE_CORS_ORIGIN_ALLOW_ALL: 'True',
|
||||
INVENTREE_BASIC_AUTH_ENABLED: 'True',
|
||||
INVENTREE_COOKIE_SAMESITE: 'False',
|
||||
INVENTREE_LOGIN_ATTEMPTS: '3',
|
||||
INVENTREE_PLUGINS_MANDATORY: 'samplelocate',
|
||||
|
||||
@@ -156,6 +156,13 @@ export function getAlerts(
|
||||
code: 'INVE-W16',
|
||||
message: t`CORS is set to allow all origins.`,
|
||||
condition: server?.system_state?.cors_allow_all || false
|
||||
},
|
||||
{
|
||||
key: 'basic_auth',
|
||||
title: t`Basic Authentication`,
|
||||
code: 'INVE-W18',
|
||||
message: t`Basic (username:password) authentication is enabled for the API.`,
|
||||
condition: server?.system_state?.basic_auth_enabled || false
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ export interface ServerAPIProps {
|
||||
};
|
||||
system_state: {
|
||||
cors_allow_all: null | boolean;
|
||||
basic_auth_enabled: null | boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user