Closes #22081: Include plaintext when creating v2 tokens via REST API (#22160)

This commit is contained in:
Jeremy Stretch 2026-05-13 11:38:17 -04:00 committed by GitHub
parent b1412514f1
commit 0ee59d7371
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 65 additions and 0 deletions

View File

@ -57,6 +57,24 @@ class TokenSerializer(ValidatedModelSerializer):
return super().validate(data)
def create(self, validated_data):
instance = super().create(validated_data)
# The plaintext token is only available in memory after save(); v2 tokens persist only an
# HMAC digest, so it can't be recovered later. Stash it on the request so to_representation()
# can return it even after the viewset re-fetches the instance from the database.
if request := self.context.get('request'):
if not hasattr(request, '_token_plaintexts'):
request._token_plaintexts = {}
request._token_plaintexts[instance.pk] = instance.token
return instance
def to_representation(self, instance):
data = super().to_representation(instance)
if not data.get('token') and (request := self.context.get('request')):
if plaintext := getattr(request, '_token_plaintexts', {}).get(instance.pk):
data['token'] = plaintext
return data
class TokenProvisionSerializer(TokenSerializer):
user = UserSerializer(

View File

@ -310,6 +310,53 @@ class TokenTestCase(
response = self.client.post(url, data, format='json', **self.header)
self.assertEqual(response.status_code, 201)
def test_create_token_returns_plaintext(self):
"""
Creating a Token via the REST API must return the usable plaintext value in the response.
For v2 tokens this value cannot be recovered later because the database stores only an
HMAC digest.
"""
self.add_permissions('users.add_token')
user = User.objects.create_user(username='token_plaintext_user')
url = reverse('users-api:token-list')
response = self.client.post(url, {'user': user.pk}, format='json', **self.header)
self.assertEqual(response.status_code, 201)
self.assertIsNotNone(response.data['token'])
self.assertEqual(len(response.data['token']), TOKEN_DEFAULT_LENGTH)
# The returned plaintext must authenticate against the stored token
token = Token.objects.get(pk=response.data['id'])
self.assertTrue(token.validate(response.data['token']))
def test_bulk_create_tokens_returns_plaintexts(self):
"""
Bulk-creating Tokens via the REST API must return the plaintext value for each created
Token in the response.
"""
self.add_permissions('users.add_token')
users = [
User.objects.create_user(username='token_bulk_user1'),
User.objects.create_user(username='token_bulk_user2'),
]
data = [{'user': u.pk} for u in users]
url = reverse('users-api:token-list')
response = self.client.post(url, data, format='json', **self.header)
self.assertEqual(response.status_code, 201)
self.assertEqual(len(response.data), len(data))
plaintexts = set()
for obj in response.data:
self.assertIsNotNone(obj['token'])
self.assertEqual(len(obj['token']), TOKEN_DEFAULT_LENGTH)
plaintexts.add(obj['token'])
token = Token.objects.get(pk=obj['id'])
self.assertTrue(token.validate(obj['token']))
# Each token should be unique
self.assertEqual(len(plaintexts), len(data))
def test_reassign_token(self):
"""
Check that a Token cannot be reassigned to another User.