Custom User Model

Difficulty: Advanced

While AbstractUser is sufficient for most projects (adding fields to the default User), some applications need fundamentally different user models. The most common case is using email as the primary identifier instead of username. This requires AbstractBaseUser, which provides only the bare minimum: password hashing and session management.

When using AbstractBaseUser, you must define several components: the model fields (including at least one unique identifier field), USERNAME_FIELD (the field used for authentication), REQUIRED_FIELDS (additional required fields for createsuperuser), is_active (required by the admin), is_staff (required for admin access), and a custom manager with create_user() and create_superuser() methods.

PermissionsMixin adds the permission-related fields and methods (groups, user_permissions, is_superuser, has_perm, has_module_perms) to your custom user model. Without it, your user model will not integrate with Django's permission system or the admin.

The custom UserManager must inherit from BaseUserManager and implement create_user() and create_superuser(). create_user() should normalize the email, create the user instance, hash the password with set_password(), and save to the database. create_superuser() should call create_user() and set is_staff and is_superuser to True.

After creating a custom user model, you need to: (1) set AUTH_USER_MODEL in settings.py, (2) create an admin class (usually extending BaseUserAdmin or UserAdmin), (3) create forms for the admin (UserCreationForm and UserChangeForm), and (4) run migrations. Remember, AUTH_USER_MODEL must be set before the first migration.

Custom authentication backends allow even more flexibility. By default, Django authenticates against the User model using username and password. You can create custom backends that authenticate using email, phone number, social accounts, or external services. Authentication backends are configured in settings.AUTHENTICATION_BACKENDS.

Code examples

Email-Based User Model

# accounts/models.py
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from django.db import models
from django.utils import timezone

class CustomUserManager(BaseUserManager):
    def create_user(self, email, password=None, extra_fields):
        if not email:
            raise ValueError('The Email field 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)

        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, password, extra_fields)

class CustomUser(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(unique=True)
    first_name = models.CharField(max_length=150, blank=True)
    last_name = models.CharField(max_length=150, blank=True)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    date_joined = models.DateTimeField(default=timezone.now)

    objects = CustomUserManager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['first_name', 'last_name']

    def __str__(self):
        return self.email

    def get_full_name(self):
        return f'{self.first_name} {self.last_name}'.strip()

    def get_short_name(self):
        return self.first_name

This email-based user model uses email as the primary identifier. USERNAME_FIELD='email' means email is used for login. REQUIRED_FIELDS are additional prompts for createsuperuser. The custom manager handles user creation with proper password hashing.

Custom User Admin

# accounts/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .models import CustomUser

@admin.register(CustomUser)
class CustomUserAdmin(BaseUserAdmin):
    list_display = ['email', 'first_name', 'last_name', 'is_staff', 'is_active']
    list_filter = ['is_staff', 'is_active', 'groups']
    search_fields = ['email', 'first_name', 'last_name']
    ordering = ['email']

    fieldsets = (
        (None, {'fields': ('email', 'password')}),
        ('Personal Info', {'fields': ('first_name', 'last_name')}),
        ('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
        ('Important dates', {'fields': ('last_login', 'date_joined')}),
    )

    add_fieldsets = (
        (None, {
            'classes': ('wide',),
            'fields': ('email', 'first_name', 'last_name', 'password1', 'password2'),
        }),
    )

Custom UserAdmin extends BaseUserAdmin with fieldsets tailored to your user model. add_fieldsets defines the form for creating new users. Since there is no username field, we use email throughout.

Custom Authentication Backend

# accounts/backends.py
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend

class EmailBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, kwargs):
        User = get_user_model()
        email = kwargs.get('email', username)
        try:
            user = User.objects.get(email=email)
        except User.DoesNotExist:
            return None

        if user.check_password(password) and self.user_can_authenticate(user):
            return user
        return None

# settings.py
AUTHENTICATION_BACKENDS = [
    'accounts.backends.EmailBackend',
]

# Usage in views:
# user = authenticate(request, email='user@example.com', password='pass123')
# Or: user = authenticate(request, username='user@example.com', password='pass123')

A custom backend extends ModelBackend and overrides authenticate(). It looks up users by email and verifies the password. user_can_authenticate() checks is_active. Multiple backends can be listed in AUTHENTICATION_BACKENDS.

Settings Configuration

# settings.py

# Point to your custom user model
AUTH_USER_MODEL = 'accounts.CustomUser'

# Custom authentication backend
AUTHENTICATION_BACKENDS = [
    'accounts.backends.EmailBackend',
]

# Auth URLs
LOGIN_URL = '/accounts/login/'
LOGIN_REDIRECT_URL = '/dashboard/'
LOGOUT_REDIRECT_URL = '/'

# Password validation
AUTH_PASSWORD_VALIDATORS = [
    {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
    {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
     'OPTIONS': {'min_length': 8}},
    {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
    {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]

AUTH_USER_MODEL must be set before the first migration. AUTHENTICATION_BACKENDS defines how authentication works. AUTH_PASSWORD_VALIDATORS enforce password strength requirements.

Key points

Concepts covered

AbstractBaseUser, PermissionsMixin, UserManager, USERNAME_FIELD, Email Auth