Clean up auth backend

This commit is contained in:
Jeremy Stretch 2025-10-03 12:08:24 -04:00
parent 917a2c2618
commit 9b85d92ad0
3 changed files with 34 additions and 83 deletions

View File

@ -166003,87 +166003,26 @@
"in": "query", "in": "query",
"name": "last_used", "name": "last_used",
"schema": { "schema": {
"type": "array",
"items": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
} }
}, },
"explode": true,
"style": "form"
},
{
"in": "query",
"name": "last_used__empty",
"schema": {
"type": "boolean"
}
},
{
"in": "query",
"name": "last_used__gt",
"schema": {
"type": "array",
"items": {
"type": "string",
"format": "date-time"
}
},
"explode": true,
"style": "form"
},
{ {
"in": "query", "in": "query",
"name": "last_used__gte", "name": "last_used__gte",
"schema": { "schema": {
"type": "array",
"items": {
"type": "string", "type": "string",
"format": "date-time" "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"
},
{ {
"in": "query", "in": "query",
"name": "last_used__lte", "name": "last_used__lte",
"schema": { "schema": {
"type": "array",
"items": {
"type": "string", "type": "string",
"format": "date-time" "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"
},
{ {
"name": "limit", "name": "limit",
"required": false, "required": false,
@ -256896,7 +256835,7 @@
"type": "apiKey", "type": "apiKey",
"in": "header", "in": "header",
"name": "Authorization", "name": "Authorization",
"description": "Set `Token <token>` (v1) or `Bearer <token>` (v2) in the Authorization header" "description": "`Token <token>` (v1) or `Bearer <key>.<token>` (v2)"
} }
} }
}, },

View File

@ -11,8 +11,8 @@ from netbox.config import get_config
from users.models import Token from users.models import Token
from utilities.request import get_client_ip from utilities.request import get_client_ip
V1_KEYWORD = 'token' V1_KEYWORD = 'Token'
V2_KEYWORD = 'bearer' V2_KEYWORD = 'Bearer'
class TokenAuthentication(BaseAuthentication): class TokenAuthentication(BaseAuthentication):
@ -22,26 +22,37 @@ class TokenAuthentication(BaseAuthentication):
model = Token model = Token
def authenticate(self, request): def authenticate(self, request):
# Ignore; Authorization header is not present
if not (auth := get_authorization_header(request).split()): if not (auth := get_authorization_header(request).split()):
return 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(): if auth[0].lower() == V1_KEYWORD.lower().encode():
version = 1 version = 1
elif auth[0].lower() == V2_KEYWORD.lower().encode(): elif auth[0].lower() == V2_KEYWORD.lower().encode():
version = 2 version = 2
else: else:
# Ignore; unrecognized header value
return return
# Extract token key from authorization header # Extract token from authorization header. This should be in one of the following two forms:
# * Authorization: Token <token> (v1)
# * Authorization: Bearer <key>.<token> (v2)
if len(auth) != 2: 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 <token>"'
)
else:
raise exceptions.AuthenticationFailed(
'Invalid authorization header: Must be in the form "Bearer <key>.<token>"'
)
# Extract the key (if v2) & token plaintext from the auth header
try: try:
auth_value = auth[1].decode() auth_value = auth[1].decode()
except UnicodeError: except UnicodeError:
raise exceptions.AuthenticationFailed("Invalid authorization header: Token contains invalid characters") raise exceptions.AuthenticationFailed("Invalid authorization header: Token contains invalid characters")
# Look for a matching token in the database
if version == 1: if version == 1:
key, plaintext = None, auth_value key, plaintext = None, auth_value
else: else:
@ -52,6 +63,8 @@ class TokenAuthentication(BaseAuthentication):
"Invalid authorization header: Could not parse key from v2 token. Did you mean to use 'Token' " "Invalid authorization header: Could not parse key from v2 token. Did you mean to use 'Token' "
"instead of 'Bearer'?" "instead of 'Bearer'?"
) )
# Look for a matching token in the database
try: try:
qs = Token.objects.prefetch_related('user') qs = Token.objects.prefetch_related('user')
if version == 1: if version == 1:
@ -61,8 +74,8 @@ class TokenAuthentication(BaseAuthentication):
# Fetch v2 token by key, then validate the plaintext # Fetch v2 token by key, then validate the plaintext
token = qs.get(version=version, key=key) token = qs.get(version=version, key=key)
if not token.validate(plaintext): if not token.validate(plaintext):
# TODO: Consider security implications of enabling validation of token key without valid plaintext # Key is valid but plaintext is not. Raise DoesNotExist to guard against key enumeration.
raise exceptions.AuthenticationFailed(f"Validation failed for v2 token {key}") raise Token.DoesNotExist()
except Token.DoesNotExist: except Token.DoesNotExist:
raise exceptions.AuthenticationFailed(f"Invalid v{version} token") raise exceptions.AuthenticationFailed(f"Invalid v{version} token")
@ -180,5 +193,5 @@ class TokenScheme(OpenApiAuthenticationExtension):
'type': 'apiKey', 'type': 'apiKey',
'in': 'header', 'in': 'header',
'name': 'Authorization', 'name': 'Authorization',
'description': 'Set `Token <token>` (v1) or `Bearer <token>` (v2) in the Authorization header', 'description': '`Token <token>` (v1) or `Bearer <key>.<token>` (v2)',
} }

View File

@ -1,5 +1,4 @@
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from social_core.storage import NO_ASCII_REGEX, NO_SPECIAL_REGEX from social_core.storage import NO_ASCII_REGEX, NO_SPECIAL_REGEX
__all__ = ( __all__ = (
@ -20,7 +19,7 @@ def get_current_pepper():
""" """
Return the ID and value of the newest (highest ID) cryptographic pepper. Return the ID and value of the newest (highest ID) cryptographic pepper.
""" """
if len(settings.API_TOKEN_PEPPERS) < 1: if not settings.API_TOKEN_PEPPERS:
raise ImproperlyConfigured("Must define API_TOKEN_PEPPERS to use v2 API tokens") raise ValueError("API_TOKEN_PEPPERS is not defined")
newest_id = sorted(settings.API_TOKEN_PEPPERS.keys())[-1] newest_id = sorted(settings.API_TOKEN_PEPPERS.keys())[-1]
return newest_id, settings.API_TOKEN_PEPPERS[newest_id] return newest_id, settings.API_TOKEN_PEPPERS[newest_id]