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'
# API tokens
TOKEN_KEY_LENGTH = 16
TOKEN_DEFAULT_LENGTH = 40
TOKEN_CHARSET = string.ascii_letters + string.digits

View File

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

View File

@ -1,8 +1,6 @@
import binascii
import hashlib
import hmac
import random
import os
from django.conf import settings
from django.contrib.postgres.fields import ArrayField
@ -16,7 +14,7 @@ from netaddr import IPNetwork
from ipam.fields import IPNetworkField
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 utilities.querysets import RestrictedQuerySet
@ -75,10 +73,11 @@ class Token(models.Model):
)
key = models.CharField(
verbose_name=_('key'),
max_length=16,
max_length=TOKEN_KEY_LENGTH,
unique=True,
blank=True,
null=True,
validators=[MinLengthValidator(TOKEN_KEY_LENGTH)],
help_text=_('v2 token identification key'),
)
pepper = models.PositiveSmallIntegerField(
@ -148,7 +147,7 @@ class Token(models.Model):
if self.v1:
self.plaintext = value
elif self.v2:
self.key = self.key or self.generate(16)
self.key = self.key or self.generate_key()
self.update_digest()
def clean(self):
@ -162,15 +161,15 @@ class Token(models.Model):
return super().save(*args, **kwargs)
@staticmethod
def generate_key():
@classmethod
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
def generate(length=40):
def generate(length=TOKEN_DEFAULT_LENGTH):
"""
Generate and return a random token value of the given length.
"""