Enforce a fixed key length for v2 tokens

This commit is contained in:
Jeremy Stretch 2025-10-02 15:26:22 -04:00
parent 1ee23ba6fa
commit 5dc48f3a88
3 changed files with 23 additions and 11 deletions

View File

@ -10,4 +10,7 @@ OBJECTPERMISSION_OBJECT_TYPES = Q(
CONSTRAINT_TOKEN_USER = '$user' CONSTRAINT_TOKEN_USER = '$user'
# API tokens
TOKEN_KEY_LENGTH = 16
TOKEN_DEFAULT_LENGTH = 40
TOKEN_CHARSET = string.ascii_letters + string.digits TOKEN_CHARSET = string.ascii_letters + string.digits

View File

@ -21,6 +21,7 @@ class Migration(migrations.Migration):
migrations.RunSQL( migrations.RunSQL(
sql="ALTER INDEX IF EXISTS users_token_key_key RENAME TO users_token_plaintext_key", sql="ALTER INDEX IF EXISTS users_token_key_key RENAME TO users_token_plaintext_key",
), ),
# Make plaintext (formerly key) nullable for v2 tokens # Make plaintext (formerly key) nullable for v2 tokens
migrations.AlterField( migrations.AlterField(
model_name='token', model_name='token',
@ -33,6 +34,7 @@ class Migration(migrations.Migration):
validators=[django.core.validators.MinLengthValidator(40)] validators=[django.core.validators.MinLengthValidator(40)]
), ),
), ),
# Add version field to distinguish v1 and v2 tokens # Add version field to distinguish v1 and v2 tokens
migrations.AddField( migrations.AddField(
model_name='token', model_name='token',
@ -40,17 +42,25 @@ class Migration(migrations.Migration):
field=models.PositiveSmallIntegerField(default=1), # Mark all existing Tokens as v1 field=models.PositiveSmallIntegerField(default=1), # Mark all existing Tokens as v1
preserve_default=False, preserve_default=False,
), ),
# Change the default version for new tokens to v2 # Change the default version for new tokens to v2
migrations.AlterField( migrations.AlterField(
model_name='token', model_name='token',
name='version', name='version',
field=models.PositiveSmallIntegerField(default=2), field=models.PositiveSmallIntegerField(default=2),
), ),
# Add new key, pepper, and hmac_digest fields for v2 tokens # Add new key, pepper, and hmac_digest fields for v2 tokens
migrations.AddField( migrations.AddField(
model_name='token', model_name='token',
name='key', name='key',
field=models.CharField(blank=True, max_length=16, null=True, unique=True), field=models.CharField(
blank=True,
max_length=16,
null=True,
unique=True,
validators=[django.core.validators.MinLengthValidator(16)]
),
), ),
migrations.AddField( migrations.AddField(
model_name='token', model_name='token',

View File

@ -1,8 +1,6 @@
import binascii
import hashlib import hashlib
import hmac import hmac
import random import random
import os
from django.conf import settings from django.conf import settings
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
@ -16,7 +14,7 @@ from netaddr import IPNetwork
from ipam.fields import IPNetworkField from ipam.fields import IPNetworkField
from users.choices import TokenVersionChoices from users.choices import TokenVersionChoices
from users.constants import TOKEN_CHARSET from users.constants import TOKEN_CHARSET, TOKEN_DEFAULT_LENGTH, TOKEN_KEY_LENGTH
from users.utils import get_current_pepper from users.utils import get_current_pepper
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
@ -75,10 +73,11 @@ class Token(models.Model):
) )
key = models.CharField( key = models.CharField(
verbose_name=_('key'), verbose_name=_('key'),
max_length=16, max_length=TOKEN_KEY_LENGTH,
unique=True, unique=True,
blank=True, blank=True,
null=True, null=True,
validators=[MinLengthValidator(TOKEN_KEY_LENGTH)],
help_text=_('v2 token identification key'), help_text=_('v2 token identification key'),
) )
pepper = models.PositiveSmallIntegerField( pepper = models.PositiveSmallIntegerField(
@ -148,7 +147,7 @@ class Token(models.Model):
if self.v1: if self.v1:
self.plaintext = value self.plaintext = value
elif self.v2: elif self.v2:
self.key = self.key or self.generate(16) self.key = self.key or self.generate_key()
self.update_digest() self.update_digest()
def clean(self): def clean(self):
@ -162,15 +161,15 @@ class Token(models.Model):
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
@staticmethod @classmethod
def generate_key(): def generate_key(cls):
""" """
DEPRECATED: Generate and return a random 160-bit key expressed in hexadecimal. Generate and return a random alphanumeric key for v2 tokens.
""" """
return binascii.hexlify(os.urandom(20)).decode() return cls.generate(length=TOKEN_KEY_LENGTH)
@staticmethod @staticmethod
def generate(length=40): def generate(length=TOKEN_DEFAULT_LENGTH):
""" """
Generate and return a random token value of the given length. Generate and return a random token value of the given length.
""" """