Fix #8839: Use USERNAME_FIELD instead of hardcoded 'username' in AuthTokenSerializer#9932
Fix #8839: Use USERNAME_FIELD instead of hardcoded 'username' in AuthTokenSerializer#9932MuraveyApp wants to merge 2 commits intoencode:mainfrom
Conversation
…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)
There was a problem hiding this comment.
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
usernametoget_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.
| 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} | ||
| ) |
There was a problem hiding this comment.
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.
| username_field = User.USERNAME_FIELD | ||
| username = attrs.get(username_field) | ||
| password = attrs.get('password') |
There was a problem hiding this comment.
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).
| 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()) | ||
|
|
There was a problem hiding this comment.
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.
| else: | ||
| msg = _('Must include "username" and "password".') | ||
| msg = _(f'Must include "{username_field}" and "password".') | ||
| raise serializers.ValidationError(msg, code='authorization') |
There was a problem hiding this comment.
_(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.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
I haven't looked at this too closely but from a quick glance:
|
Summary
Fixes #8839 —
AuthTokenSerializerhardcodedusernamefield, breaking token auth for projects using customUSERNAME_FIELD(e.g. email, phone).Changes
In
rest_framework/authtoken/serializers.py:__init__that dynamically renames the field based onget_user_model().USERNAME_FIELDvalidate()to authenticate using the correct field nameBefore/After
Context
This issue has been open since January 2023 (3 years). Resolved with assistance from OwlMind — an autonomous AI coding agent.