From 9b85d92ad0e066ee9b633415a38b04861fadf71c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 3 Oct 2025 12:08:24 -0400 Subject: [PATCH] Clean up auth backend --- contrib/openapi.json | 79 ++++------------------------- netbox/netbox/api/authentication.py | 33 ++++++++---- netbox/users/utils.py | 5 +- 3 files changed, 34 insertions(+), 83 deletions(-) diff --git a/contrib/openapi.json b/contrib/openapi.json index 7fda93ac5..810f9936d 100644 --- a/contrib/openapi.json +++ b/contrib/openapi.json @@ -166003,86 +166003,25 @@ "in": "query", "name": "last_used", "schema": { - "type": "array", - "items": { - "type": "string", - "format": "date-time" - } - }, - "explode": true, - "style": "form" - }, - { - "in": "query", - "name": "last_used__empty", - "schema": { - "type": "boolean" + "type": "string", + "format": "date-time" } }, - { - "in": "query", - "name": "last_used__gt", - "schema": { - "type": "array", - "items": { - "type": "string", - "format": "date-time" - } - }, - "explode": true, - "style": "form" - }, { "in": "query", "name": "last_used__gte", "schema": { - "type": "array", - "items": { - "type": "string", - "format": "date-time" - } - }, - "explode": true, - "style": "form" - }, - { - "in": "query", - "name": "last_used__lt", - "schema": { - "type": "array", - "items": { - "type": "string", - "format": "date-time" - } - }, - "explode": true, - "style": "form" + "type": "string", + "format": "date-time" + } }, { "in": "query", "name": "last_used__lte", "schema": { - "type": "array", - "items": { - "type": "string", - "format": "date-time" - } - }, - "explode": true, - "style": "form" - }, - { - "in": "query", - "name": "last_used__n", - "schema": { - "type": "array", - "items": { - "type": "string", - "format": "date-time" - } - }, - "explode": true, - "style": "form" + "type": "string", + "format": "date-time" + } }, { "name": "limit", @@ -256896,7 +256835,7 @@ "type": "apiKey", "in": "header", "name": "Authorization", - "description": "Set `Token ` (v1) or `Bearer ` (v2) in the Authorization header" + "description": "`Token ` (v1) or `Bearer .` (v2)" } } }, diff --git a/netbox/netbox/api/authentication.py b/netbox/netbox/api/authentication.py index 9c73259bf..27247169a 100644 --- a/netbox/netbox/api/authentication.py +++ b/netbox/netbox/api/authentication.py @@ -11,8 +11,8 @@ from netbox.config import get_config from users.models import Token from utilities.request import get_client_ip -V1_KEYWORD = 'token' -V2_KEYWORD = 'bearer' +V1_KEYWORD = 'Token' +V2_KEYWORD = 'Bearer' class TokenAuthentication(BaseAuthentication): @@ -22,26 +22,37 @@ class TokenAuthentication(BaseAuthentication): model = Token def authenticate(self, request): + # Ignore; Authorization header is not present if not (auth := get_authorization_header(request).split()): return - # Check for Token/Bearer keyword in HTTP header value & infer token version + # Infer token version from Token/Bearer keyword in HTTP header if auth[0].lower() == V1_KEYWORD.lower().encode(): version = 1 elif auth[0].lower() == V2_KEYWORD.lower().encode(): version = 2 else: + # Ignore; unrecognized header value return - # Extract token key from authorization header + # Extract token from authorization header. This should be in one of the following two forms: + # * Authorization: Token (v1) + # * Authorization: Bearer . (v2) if len(auth) != 2: - raise exceptions.AuthenticationFailed("Invalid authorization header: Error parsing token") + if version == 1: + raise exceptions.AuthenticationFailed( + 'Invalid authorization header: Must be in the form "Token "' + ) + else: + raise exceptions.AuthenticationFailed( + 'Invalid authorization header: Must be in the form "Bearer ."' + ) + + # Extract the key (if v2) & token plaintext from the auth header try: auth_value = auth[1].decode() except UnicodeError: raise exceptions.AuthenticationFailed("Invalid authorization header: Token contains invalid characters") - - # Look for a matching token in the database if version == 1: key, plaintext = None, auth_value else: @@ -52,6 +63,8 @@ class TokenAuthentication(BaseAuthentication): "Invalid authorization header: Could not parse key from v2 token. Did you mean to use 'Token' " "instead of 'Bearer'?" ) + + # Look for a matching token in the database try: qs = Token.objects.prefetch_related('user') if version == 1: @@ -61,8 +74,8 @@ class TokenAuthentication(BaseAuthentication): # Fetch v2 token by key, then validate the plaintext token = qs.get(version=version, key=key) if not token.validate(plaintext): - # TODO: Consider security implications of enabling validation of token key without valid plaintext - raise exceptions.AuthenticationFailed(f"Validation failed for v2 token {key}") + # Key is valid but plaintext is not. Raise DoesNotExist to guard against key enumeration. + raise Token.DoesNotExist() except Token.DoesNotExist: raise exceptions.AuthenticationFailed(f"Invalid v{version} token") @@ -180,5 +193,5 @@ class TokenScheme(OpenApiAuthenticationExtension): 'type': 'apiKey', 'in': 'header', 'name': 'Authorization', - 'description': 'Set `Token ` (v1) or `Bearer ` (v2) in the Authorization header', + 'description': '`Token ` (v1) or `Bearer .` (v2)', } diff --git a/netbox/users/utils.py b/netbox/users/utils.py index 5db8cb65e..c355873a8 100644 --- a/netbox/users/utils.py +++ b/netbox/users/utils.py @@ -1,5 +1,4 @@ from django.conf import settings -from django.core.exceptions import ImproperlyConfigured from social_core.storage import NO_ASCII_REGEX, NO_SPECIAL_REGEX __all__ = ( @@ -20,7 +19,7 @@ def get_current_pepper(): """ Return the ID and value of the newest (highest ID) cryptographic pepper. """ - if len(settings.API_TOKEN_PEPPERS) < 1: - raise ImproperlyConfigured("Must define API_TOKEN_PEPPERS to use v2 API tokens") + if not settings.API_TOKEN_PEPPERS: + raise ValueError("API_TOKEN_PEPPERS is not defined") newest_id = sorted(settings.API_TOKEN_PEPPERS.keys())[-1] return newest_id, settings.API_TOKEN_PEPPERS[newest_id]