Using email as the login field in Django REST Framework
In most projects I work on, people sign in with their email and a password. The default Django User model still has a username field though, which means you either have to keep email and username in sync or build some custom login logic.
In this post I will show how I usually set up a custom user model that:
- Uses email as the unique identifier
- Keeps everything working nicely with Django admin
- Plays well with Django REST Framework
We will not go into every internal detail of Django authentication. This is a practical setup that you can drop into a new project and adapt.
Important: Do this at the start of your project. Changing the user model after you have real data is painful. Changing the user model after you have real data is technically possible, but painful.
Do you really need a custom user model?
A custom user model is useful if:
- Users should log in with email instead of username
- You know you will need extra fields on the user
- You want to enforce one unique identifier across the system
You might not need it if:
- Your app is tiny and admin only
- The default username works fine for your use case
- You are integrating with an external identity provider
If you are unsure, I would still lean toward using a custom user model on new projects.
Custom user manager
Before we create our model, we need to create a manager for our user. This enables us to still use the command line tools properly, such as python manage.py createsuperuser.
This basically just checks that the email address is set when creating the new user, since the default behavior is to allow for this to allow blank email addresses.
from django.contrib.auth.base_user import BaseUserManager
class AppUserManager(BaseUserManager):
def create_user(self, email, password=None, **extra_fields):
if not email:
raise ValueError("The email address must be set")
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password=None, **extra_fields):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
extra_fields.setdefault("is_active", True)
if extra_fields.get("is_staff") is not True:
raise ValueError("Superuser must have is_staff=True.")
if extra_fields.get("is_superuser") is not True:
raise ValueError("Superuser must have is_superuser=True.")
return self.create_user(email=email, password=password, **extra_fields)
Custom user model
Our user model will inherit from AbstractUser. We can then remove the username field, and tell it to use the email field as the new username field.
The username field is generally required as well, so we set REQUIRED_FIELDS to an empty list.
In models.py, you can create the following:
import uuid
from django.contrib.auth.models import AbstractUser
from django.db import models
from .managers import AppUserManager
class AppUser(AbstractUser):
username = None
user_id = models.UUIDField(default=uuid.uuid4, editable=False)
email = models.EmailField(unique=True)
USERNAME_FIELD = "email"
REQUIRED_FIELDS = []
objects = AppUserManager()
def __str__(self):
return self.email
If you don't want to override the unique identifier for your user model, you can omit the
user_idline above.
Settings
Next, we need to let Django know that it would use the custom user model we’ve defined above.
In your settings.py, add the following:
AUTH_USER_MODEL = "users.AppUser"
Serializers
Next, we will create serializers to handle validation and creation. The RegistrationSerializer defines fields that will be required for users to sign up, and the Userserializer will be used to format our responses when sending user information in responses.
from django.contrib.auth import get_user_model
from rest_framework import serializers
from rest_framework.validators import UniqueValidator
AppUser = get_user_model()
class RegistrationSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, min_length=8)
email = serializers.EmailField(
validators=[UniqueValidator(queryset=AppUser.objects.all())]
)
class Meta:
model = AppUser
fields = ("email", "password", "first_name", "last_name")
def create(self, validated_data):
password = validated_data.pop("password")
user = AppUser.objects.create_user(password=password, **validated_data)
return user
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = AppUser
fields = (
"id",
"user_id",
"email",
"first_name",
"last_name",
"date_joined",
"is_active",
)
read_only_fields = ("id", "user_id", "date_joined", "is_active")
Registration endpoint
Now that we have most of what we need, we can create an endpoint that allows for user registration. This will use the registration serializer for input and the public user serializer for output.
from rest_framework import generics, permissions
from .serializers import RegistrationSerializer, UserSerializer
from django.contrib.auth import get_user_model
AppUser = get_user_model()
class RegisterView(generics.CreateAPIView):
queryset = AppUser.objects.all()
serializer_class = RegistrationSerializer
permission_classes = (permissions.AllowAny,)
def perform_create(self, serializer):
self.created_user = serializer.save()
def get_serializer_class(self):
if self.request.method.lower() == "post":
return RegistrationSerializer
return UserSerializer
def create(self, request, *args, **kwargs):
response = super().create(request, *args, **kwargs)
user_data = UserSerializer(self.created_user).data
response.data = user_data
return response
You can also keep it even simpler and just return the registration serializer’s data. I prefer a separate UserSerializer so I can reuse it elsewhere.
URLs
You can add this to your urls.py file to register the endpoint:
path("register/", RegisterView.as_view()),
Admin forms
To manage users in the Django admin you can wire up a couple of simple forms in users/forms.py.
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import AppUser
class AppUserCreationForm(UserCreationForm):
class Meta(UserCreationForm.Meta):
model = AppUser
fields = ("email", "first_name", "last_name")
class AppUserChangeForm(UserChangeForm):
class Meta:
model = AppUser
fields = ("email", "first_name", "last_name", "is_active", "is_staff")
Admin registration
Finally, tell the admin how to use your custom forms and user model in users/admin.py.
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .forms import AppUserCreationForm, AppUserChangeForm
from .models import AppUser
@admin.register(AppUser)
class AppUserAdmin(UserAdmin):
add_form = AppUserCreationForm
form = AppUserChangeForm
model = AppUser
list_display = ("email", "first_name", "last_name", "is_staff", "is_active")
list_filter = ("is_staff", "is_active")
fieldsets = (
(None, {"fields": ("email", "password")}),
("Personal info", {"fields": ("first_name", "last_name")}),
("Permissions", {"fields": ("is_active", "is_staff", "is_superuser", "groups")}),
("Important dates", {"fields": ("last_login", "date_joined")}),
)
add_fieldsets = (
(
None,
{
"classes": ("wide",),
"fields": (
"email",
"first_name",
"last_name",
"password1",
"password2",
"is_staff",
"is_active",
),
},
),
)
search_fields = ("email",)
ordering = ("email",)
Wrap
You now have a clean custom user model that uses email as the primary login field, works with Django admin, and integrates nicely with Django REST Framework. This forms a solid base for authentication, onboarding flows, and more complex user features.
If you have this custom user model in place, the next step is to decide how clients authenticate. In the next article I will walk through adding JWT based authentication on top of this setup.