Skip to content

Fix #8839: Use USERNAME_FIELD instead of hardcoded 'username' in AuthTokenSerializer#9932

Open
MuraveyApp wants to merge 2 commits intoencode:mainfrom
MuraveyApp:fix/issue-8839
Open

Fix #8839: Use USERNAME_FIELD instead of hardcoded 'username' in AuthTokenSerializer#9932
MuraveyApp wants to merge 2 commits intoencode:mainfrom
MuraveyApp:fix/issue-8839

Conversation

@MuraveyApp
Copy link
Copy Markdown

Summary

Fixes #8839AuthTokenSerializer hardcoded username field, breaking token auth for projects using custom USERNAME_FIELD (e.g. email, phone).

Changes

In rest_framework/authtoken/serializers.py:

  • Added __init__ that dynamically renames the field based on get_user_model().USERNAME_FIELD
  • Updated validate() to authenticate using the correct field name
  • Error messages now reference the actual field name

Before/After

# Before: only works with username
POST /api-token-auth/ {"username": "john", "password": "..."}

# After: works with any USERNAME_FIELD
POST /api-token-auth/ {"email": "john@example.com", "password": "..."}

Context

This issue has been open since January 2023 (3 years). Resolved with assistance from OwlMind — an autonomous AI coding agent.

…ardcoded 'username'

ObtainAuthToken serializer hardcoded 'username' field, breaking token auth
for projects using custom USERNAME_FIELD (e.g. email, phone number).

Now dynamically uses get_user_model().USERNAME_FIELD:
- __init__: renames the field if USERNAME_FIELD != 'username'
- validate: authenticates using the correct field name
- Error messages reference the actual field name

This issue has been open since January 2023 (3 years).
Resolved with assistance from OwlMind (https://owlmind.dev)
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates DRF’s AuthTokenSerializer to use the active user model’s USERNAME_FIELD instead of hardcoding "username", restoring token auth compatibility for projects with custom login identifiers (e.g., email).

Changes:

  • Dynamically renames the serializer’s login field from username to get_user_model().USERNAME_FIELD.
  • Authenticates using the correct credential keyword argument derived from USERNAME_FIELD.
  • Updates the “missing credentials” validation message to reference the configured login field name.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +23 to +41
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
User = get_user_model()
username_field = User.USERNAME_FIELD
if username_field != 'username':
self.fields[username_field] = self.fields.pop('username')
self.fields[username_field].label = _(username_field.capitalize())

def validate(self, attrs):
username = attrs.get('username')
User = get_user_model()
username_field = User.USERNAME_FIELD
username = attrs.get(username_field)
password = attrs.get('password')

if username and password:
user = authenticate(request=self.context.get('request'),
username=username, password=password)
user = authenticate(
request=self.context.get('request'),
**{username_field: username, 'password': password}
)
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New behavior for non-default USERNAME_FIELD (renaming the input field and authenticating via **{username_field: ...}) isn’t covered by tests. Since this is a regression fix for custom user models, add a test that patches get_user_model().USERNAME_FIELD (e.g. to email) and asserts the serializer accepts {email, password} and calls authenticate with the expected keyword argument.

Copilot uses AI. Check for mistakes.
Comment on lines +33 to 35
username_field = User.USERNAME_FIELD
username = attrs.get(username_field)
password = attrs.get('password')
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The local variable name username is now potentially an email/phone/etc (depending on USERNAME_FIELD), which makes the control flow harder to follow. Consider renaming it to something neutral like identifier/login (and similarly for username_field if you store it on self).

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +30
username_field = User.USERNAME_FIELD
if username_field != 'username':
self.fields[username_field] = self.fields.pop('username')
self.fields[username_field].label = _(username_field.capitalize())

Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.fields[username_field].label = _(username_field.capitalize()) builds a dynamic translation key at runtime, so it won’t be picked up by Django’s i18n tooling and will remain untranslated. Also, capitalize() produces awkward labels for fields like phone_number. Prefer using the user model field’s verbose_name (e.g., User._meta.get_field(username_field).verbose_name) and avoid wrapping dynamically generated strings in gettext_lazy.

Copilot uses AI. Check for mistakes.
Comment on lines 49 to 51
else:
msg = _('Must include "username" and "password".')
msg = _(f'Must include "{username_field}" and "password".')
raise serializers.ValidationError(msg, code='authorization')
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_(f'Must include "{username_field}" and "password".') creates a different msgid depending on USERNAME_FIELD, which breaks translations. Use a constant translatable string with interpolation (e.g., a %(username_field)s placeholder) and consider using the field’s human-readable label/verbose_name in the message rather than the internal model field name.

Copilot uses AI. Check for mistakes.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@browniebroke
Copy link
Copy Markdown
Member

I haven't looked at this too closely but from a quick glance:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

use USERNAME_FIELD instead username in generate token for user in token authentication

4 participants