Skip to content
10 changes: 10 additions & 0 deletions rest_framework/authtoken/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,18 @@ class Meta:
verbose_name_plural = _("Tokens")

def save(self, *args, **kwargs):
"""
Save the token instance.

If no key is provided, generates a cryptographically secure key.
For existing tokens with cleared keys, regenerates the key.
For new tokens, ensures they are inserted as new (not updated).
"""
if not self.key:
self.key = self.generate_key()
# For new objects, force INSERT to prevent overwriting existing tokens
if self._state.adding:
kwargs['force_insert'] = True
return super().save(*args, **kwargs)

@classmethod
Expand Down
59 changes: 59 additions & 0 deletions tests/test_authtoken.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import importlib
from io import StringIO
from unittest import mock

import pytest
from django.contrib.admin import site
from django.contrib.auth.models import User
from django.core.management import CommandError, call_command
from django.db import IntegrityError
from django.test import TestCase, modify_settings

from rest_framework.authtoken.admin import TokenAdmin
Expand Down Expand Up @@ -49,6 +51,63 @@ def test_whitespace_in_password(self):
assert AuthTokenSerializer(data=data).is_valid()



def test_token_creation_collision_raises_integrity_error(self):
"""
Verify that creating a token with an existing key raises IntegrityError.
"""
user2 = User.objects.create_user('user2', '[email protected]', 'p')
existing_token = Token.objects.create(user=user2)

# Try to create another token with the same key
with self.assertRaises(IntegrityError):
Token.objects.create(key=existing_token.key, user=self.user)

def test_key_regeneration_on_save_is_not_a_breaking_change(self):
"""
Verify that when a token is created without a key, it generates one correctly.
This tests the backward compatibility scenario where existing code might
create tokens without explicitly setting a key.
"""
# Create a new user for this test to avoid conflicts with setUp token
user2 = User.objects.create_user('test_user2', '[email protected]', 'password')

# Create a token without a key - it should generate one automatically
token = Token(user=user2)
token.key = "" # Explicitly clear the key
token.save()

# Verify the key was generated
self.assertEqual(len(token.key), 40)
self.assertEqual(token.user, user2)

# Verify it's saved in the database
token.refresh_from_db()
self.assertEqual(len(token.key), 40)
self.assertEqual(token.user, user2)

def test_saving_existing_token_without_changes_does_not_alter_key(self):
"""
Ensure that calling save() on an existing token without modifications
does not change its key.
"""
original_key = self.token.key

self.token.save()
self.assertEqual(self.token.key, original_key)

def test_generate_key_uses_os_urandom(self):
"""
Verify that `generate_key` correctly calls `os.urandom`.
"""
with mock.patch('rest_framework.authtoken.models.os.urandom') as mock_urandom:
mock_urandom.return_value = b'a_mocked_key_of_proper_length_0123456789'
key = Token.generate_key()

mock_urandom.assert_called_once_with(20)
self.assertEqual(key, '615f6d6f636b65645f6b65795f6f665f70726f7065725f6c656e6774685f30313233343536373839')


class AuthTokenCommandTests(TestCase):

def setUp(self):
Expand Down
Loading