← Back to writing
djangorest frameworkauthenticationcustom user modelbackend development

Customising User Models in Django REST Framework

·5 min read

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_id line 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.

Found this useful? Consider sharing it with your team.

← Back to all articles