Skip to content

Conversation

@partho-debnath
Copy link

Summary

Django’s default user model uses username and password for authentication. However, when a custom user model is defined with USERNAME_FIELD = 'email', Django correctly uses email and password for authentication.

The issue was that Django REST Framework's obtain_auth_token endpoint (used for TokenAuthentication) still expected username and password, even when a custom user model used email instead of username.

Fix

This update modifies the ObtainAuthToken view to dynamically use USERNAME_FIELD instead of assuming username. Now, authentication works consistently, whether using the built-in User model or a custom one, with no inconsistencies.

Changes Made:

  1. ObtainAuthToken now retrieves the USERNAME_FIELD from the user model.
  2. It authenticates using USERNAME_FIELD and password, ensuring compatibility with both built-in and custom user models.

Impact

  1. Users with a custom User model (e.g., USERNAME_FIELD = 'email') can now log in using email and password instead of username and password.
  2. Users with the default User model or a custom User model where USERNAME_FIELD = 'username' can continue logging in using username and password as expected.

…password' instead of 'username' and 'password' for both the built-in and custom User models
@partho-debnath
Copy link
Author

If any changes are required, please let me know.

@browniebroke
Copy link
Member

I'm not sure this falls under our current maintenance policy:

At this point in its lifespan we consider Django REST framework to be feature-complete. We focus on pull requests that track the continued development of Django versions, and generally do not accept new features or code formatting changes.

One could argue that this improves compatibility with custom Django user models, but on the other hand USERNAME_FIELD has been in Django for so long that it's a bit late to add this now...

It's also simple to customise in user-land right now, and this is explained in our docs.

If we were to ever accept it, we would need some tests to cover the behaviour with a customised user model...

@partho-debnath
Copy link
Author

partho-debnath commented Mar 31, 2025

Thank you for reviewing my pull request and for your feedback. I understand that Django REST Framework is considered feature-complete and that new features are usually only accepted if they align with Django’s ongoing development.

However, I see this change as more of a compatibility improvement rather than a new feature. While USERNAME_FIELD has been in Django for a long time, ObtainAuthToken still assumes a username by default. This creates an inconsistency for projects that take advantage of Django’s built-in flexibility for custom user models.

@browniebroke, Are you saying that it won't be possible to accept or merge this change?

Copy link
Collaborator

@auvipy auvipy left a comment

Choose a reason for hiding this comment

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

I think this might break existing projects or functionalities

@stale
Copy link

stale bot commented Oct 18, 2025

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale label Oct 18, 2025
@auvipy auvipy requested a review from Copilot December 14, 2025 15:04
@auvipy auvipy removed the stale label Dec 14, 2025
Copy link

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

This PR modifies the ObtainAuthToken view and AuthTokenSerializer to support custom Django user models that use a different USERNAME_FIELD (e.g., 'email' instead of 'username'). The changes enable dynamic authentication field detection based on the user model's USERNAME_FIELD attribute.

  • Dynamically retrieves the USERNAME_FIELD from the active user model instead of hardcoding 'username'
  • Updates the serializer to create appropriate fields (EmailField or CharField) based on the USERNAME_FIELD type
  • Modifies the API schema generation to reflect the correct field name in documentation

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 10 comments.

File Description
rest_framework/authtoken/serializers.py Refactored to dynamically create authentication fields based on USERNAME_FIELD and pass credentials to authenticate()
rest_framework/authtoken/views.py Updated schema definition to use USERNAME_FIELD for API documentation
docs/api-guide/authentication.md Added documentation note about custom user model support

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

)
def __init__(self, instance=None, data=None, **kwargs):
super().__init__(instance, data=data, **kwargs)
self.identifier_fiend_name = USER_MODEL.USERNAME_FIELD
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

Typo in variable name: 'identifier_fiend_name' should be 'identifier_field_name'. This typo appears throughout the class and will cause the code to break.

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +14
if USER_MODEL.get_email_field_name() == self.identifier_fiend_name:
self.fields[self.identifier_fiend_name] = serializers.EmailField(
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

Typo in variable name: 'identifier_fiend_name' should be 'identifier_field_name'. This typo appears throughout the class and will cause the code to break.

Copilot uses AI. Check for mistakes.
write_only=True
)
else:
self.fields[self.identifier_fiend_name] = serializers.CharField(
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

Typo in variable name: 'identifier_fiend_name' should be 'identifier_field_name'. This typo appears throughout the class and will cause the code to break.

Copilot uses AI. Check for mistakes.
username=username, password=password)
if identifier_value and password:
credentials = {
self.identifier_fiend_name: identifier_value,
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

Typo in variable name: 'identifier_fiend_name' should be 'identifier_field_name'. This typo appears throughout the class and will cause the code to break.

Copilot uses AI. Check for mistakes.

from rest_framework import serializers

USER_MODEL = get_user_model()
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The USER_MODEL is evaluated at module import time, which means if the user model changes dynamically or if this module is imported before Django apps are ready, it could reference the wrong model. Consider moving this inside the init method where it's actually used, similar to how it's done in the views.py file.

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +28
def __init__(self, instance=None, data=None, **kwargs):
super().__init__(instance, data=data, **kwargs)
self.identifier_fiend_name = USER_MODEL.USERNAME_FIELD
if USER_MODEL.get_email_field_name() == self.identifier_fiend_name:
self.fields[self.identifier_fiend_name] = serializers.EmailField(
label=_(self.identifier_fiend_name.title()),
write_only=True
)
else:
self.fields[self.identifier_fiend_name] = serializers.CharField(
label=_(self.identifier_fiend_name.title()),
write_only=True
)
self.fields["password"] = serializers.CharField(
label=_("Password"),
style={'input_type': 'password'},
trim_whitespace=False,
write_only=True
)
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The new behavior for supporting custom user models with different USERNAME_FIELD values (e.g., 'email') lacks test coverage. Consider adding tests that verify the serializer correctly handles custom user models where USERNAME_FIELD is set to a field other than 'username', such as 'email'.

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +22
USER_MODEL = get_user_model()
identifier_field_name = USER_MODEL.USERNAME_FIELD
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The USER_MODEL and identifier_field_name are evaluated at class definition time inside the conditional block. This means they are computed once when the module is imported. If the user model is swapped or changed dynamically, this will not be reflected. Consider moving this logic to a method or property that evaluates at runtime.

Copilot uses AI. Check for mistakes.
Comment on lines 23 to 32
schema = ManualSchema(
fields=[
coreapi.Field(
name="username",
name=identifier_field_name,
required=True,
location='form',
schema=coreschema.String(
title="Username",
description="Valid username for authentication",
title=identifier_field_name.title(),
description=f"Valid {identifier_field_name} for authentication",
),
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The new dynamic schema generation based on USERNAME_FIELD lacks test coverage. Consider adding tests that verify the schema correctly reflects the USERNAME_FIELD from custom user models (e.g., when USERNAME_FIELD is 'email', the schema should show 'email' instead of 'username').

Copilot uses AI. Check for mistakes.

def validate(self, attrs):
username = attrs.get('username')
identifier_value = attrs.get(self.identifier_fiend_name)
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

Typo in variable name: 'identifier_fiend_name' should be 'identifier_field_name'. This typo appears throughout the class and will cause the code to break.

Copilot uses AI. Check for mistakes.
raise serializers.ValidationError(msg, code='authorization')
else:
msg = _('Must include "username" and "password".')
msg = _(f'Must include "{self.identifier_fiend_name}" and "password".')
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

Typo in variable name: 'identifier_fiend_name' should be 'identifier_field_name'. This typo appears throughout the class and will cause the code to break.

Copilot uses AI. Check for mistakes.
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.

3 participants