mirror of
https://github.com/inventree/InvenTree.git
synced 2026-11-10 02:47:49 +00:00
Token improvements (#13155)
- More secure masking of tokens - Fix lowercase bug - Use consistent date checking
This commit is contained in:
1 parent
61a2c2af5f
commit
1b8bdad061
6 files changed
+60
-10
No files matched your search
@@ -1180,8 +1180,18 @@ def sanitize_token(token_value: str, front=8, back=12) -> str:
|
||||
|
||||
Returns:
|
||||
The sanitized token string
|
||||
|
||||
Note:
|
||||
Values too short to keep the 'front' and 'back' sections visible
|
||||
are almost entirely masked, so that the full value is never exposed.
|
||||
"""
|
||||
middle = len(token_value) - (front + back)
|
||||
length = len(token_value)
|
||||
|
||||
if length <= front + back:
|
||||
visible = min(front, length // 4)
|
||||
return token_value[:visible] + '*' * (length - visible)
|
||||
|
||||
middle = length - (front + back)
|
||||
return token_value[:front] + '*' * middle + token_value[-back:]
|
||||
|
||||
|
||||
|
||||
@@ -38,10 +38,11 @@ def get_token_from_request(request):
|
||||
|
||||
for k in auth_keys:
|
||||
if auth_header := request.headers.get(k, None):
|
||||
auth_header = auth_header.strip().lower().split()
|
||||
# Only the scheme is case-insensitive - the token value must be preserved
|
||||
auth_header = auth_header.split()
|
||||
|
||||
if len(auth_header) > 1:
|
||||
if auth_header[0].strip().lower().replace(':', '') in token_keys:
|
||||
if auth_header[0].lower().replace(':', '') in token_keys:
|
||||
token = auth_header[1]
|
||||
return token
|
||||
|
||||
|
||||
@@ -163,6 +163,23 @@ class MiddlewareTests(InvenTreeTestCase):
|
||||
|
||||
self.assertIn('Invalid token', '\n'.join(captured.output))
|
||||
|
||||
def test_token_auth_case(self):
|
||||
"""The auth scheme is case-insensitive, but the token value is not."""
|
||||
target = reverse('auth-check')
|
||||
|
||||
raw_token = ApiToken.objects.create(user=self.user, name='case-test').token
|
||||
self.client.logout()
|
||||
|
||||
for scheme in ['Token', 'TOKEN', 'token', 'bearer', 'Bearer:']:
|
||||
self.check_path(target, auth_header=f'{scheme} {raw_token}')
|
||||
|
||||
self.check_path(target, 401, auth_header=f'Token {raw_token.upper()}')
|
||||
|
||||
# A (legacy) mixed-case key must not be lowercased before lookup
|
||||
key = 'inv-' + 'AbCdEf0123456789' * 2
|
||||
ApiToken.objects.create(user=self.user, name='legacy', key=key)
|
||||
self.check_path(target, auth_header=f'Token {key}')
|
||||
|
||||
def test_info_view_inactive_staff(self):
|
||||
"""InfoView must not treat a token for an inactive staff user as staff."""
|
||||
self.user.is_staff = True
|
||||
|
||||
@@ -1716,6 +1716,28 @@ class BarcodeMixinTest(InvenTreeTestCase):
|
||||
class SanitizerTest(TestCase):
|
||||
"""Simple tests for sanitizer functions."""
|
||||
|
||||
def test_sanitize_token(self):
|
||||
"""Test that sanitize_token never exposes a full (or near-full) value."""
|
||||
sanitize = InvenTree.helpers.sanitize_token
|
||||
|
||||
# Long values keep the front and back sections
|
||||
value = 'inv-2-' + 'a' * 20 + '.' + 'b' * 40 + '-20261101'
|
||||
result = sanitize(value)
|
||||
self.assertEqual(len(result), len(value))
|
||||
self.assertTrue(result.startswith(value[:8]))
|
||||
self.assertTrue(result.endswith(value[-12:]))
|
||||
self.assertIn('*' * (len(value) - 20), result)
|
||||
|
||||
# Short values are almost entirely masked
|
||||
for value in ['', 'a', 'abcd123', 'x' * 20, 'y' * 21]:
|
||||
result = sanitize(value)
|
||||
self.assertEqual(len(result), len(value))
|
||||
if value:
|
||||
self.assertNotEqual(result, value)
|
||||
|
||||
self.assertEqual(sanitize('abcd123'), 'a******')
|
||||
self.assertEqual(sanitize('0123456789abcdefghij'), '01234*' + '*' * 14)
|
||||
|
||||
def test_svg_sanitizer(self):
|
||||
"""Test that SVGs are sanitized accordingly."""
|
||||
valid_string = """<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="svg2" height="400" width="400">{0}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""DRF API definition for the 'users' app."""
|
||||
|
||||
import datetime
|
||||
|
||||
from django.contrib.auth import get_user, login
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.contrib.auth.password_validation import password_changed, validate_password
|
||||
@@ -20,6 +18,7 @@ from rest_framework import exceptions
|
||||
from rest_framework.generics import DestroyAPIView, GenericAPIView
|
||||
from rest_framework.response import Response
|
||||
|
||||
import InvenTree.helpers
|
||||
import InvenTree.permissions
|
||||
from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration
|
||||
from InvenTree.filters import SEARCH_ORDER_FILTER
|
||||
@@ -408,7 +407,7 @@ class GetAuthToken(GenericAPIView):
|
||||
|
||||
name = ApiToken.sanitize_name(name)
|
||||
|
||||
today = datetime.date.today()
|
||||
today = InvenTree.helpers.current_date()
|
||||
reissue_token = request.resolver_match.url_name == 'api-token'
|
||||
|
||||
token = ApiToken.objects.filter(
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
"""Custom token authentication class for InvenTree API."""
|
||||
|
||||
import datetime
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from oauth2_provider.contrib.rest_framework import OAuth2ProtectedResourceAuthentication
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.authentication import TokenAuthentication
|
||||
|
||||
import InvenTree.helpers
|
||||
import users.models
|
||||
|
||||
|
||||
@@ -34,11 +33,13 @@ def validate_api_token(raw_token: str) -> users.models.ApiToken:
|
||||
if token.expired:
|
||||
raise exceptions.AuthenticationFailed(_('Token has expired'))
|
||||
|
||||
if token.last_seen != datetime.date.today():
|
||||
today = InvenTree.helpers.current_date()
|
||||
|
||||
if token.last_seen != today:
|
||||
# Update the last-seen date
|
||||
# Note: Use update_fields to avoid clobbering concurrent changes to
|
||||
# other fields on this token (e.g. a concurrent revocation)
|
||||
token.last_seen = datetime.date.today()
|
||||
token.last_seen = today
|
||||
token.save(update_fields=['last_seen'])
|
||||
|
||||
return token
|
||||
|
||||
Reference in new issue
Block a user