Django Custom User Models and Authentication Complete Guide
How to replace Django's default user model with one built for your application — email-based login, custom fields, a proper manager — and how the authentication stack fits together around it.
Django ships a usable User model, but it hard-codes a required unique username and a 150-character limit, and it assumes username-based login. Most real applications want email as the identifier, which means a custom user model.
There is one critical constraint that shapes everything below: the user model is extremely difficult to change after you have run migrations. Getting this right at project start costs an hour; getting it wrong costs a data migration across every table with a foreign key to users.
Why the User Model Must Be Decided Before the First Migration
AUTH_USER_MODEL is read when migrations are generated, and every ForeignKey to the user is baked into those migration files. Swapping the model afterwards means Django wants to drop and recreate relations across the whole schema.
AUTH_USER_MODEL and create your user app before running migrate for the first time. On an existing project, the practical options are a bespoke data migration or starting the database over — neither is quick.# settings.py
AUTH_USER_MODEL = "accounts.User"
# In models.py — never import User directly
from django.conf import settings
from django.db import models
class Post(models.Model):
# CORRECT: resolved from AUTH_USER_MODEL at migration time
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="posts",
)
# In views, forms and tests — use the accessor
from django.contrib.auth import get_user_model
User = get_user_model()
The rule is simple and absolute: settings.AUTH_USER_MODEL in models, get_user_model() everywhere else. A direct from django.contrib.auth.models import User import will break the moment the model changes.
Choosing How Much to Replace
Django offers two starting points. AbstractUser keeps the existing field set and lets you add to it. AbstractBaseUser gives you only the password and last-login plumbing, leaving the identity fields entirely up to you.
| Base class | Gives you | Choose when |
|---|---|---|
AbstractUser | username, email, first/last name, permissions | You want extra fields and are content with username |
AbstractBaseUser | password, last_login only | You want email-only login or a different identity field |
models.Model profile | A OneToOne to the user | You only need extra data, not a different identity |
The simplest case is adding fields while keeping Django's defaults.
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
bio = models.TextField(blank=True)
avatar = models.ImageField(upload_to="avatars/", blank=True, null=True)
is_verified = models.BooleanField(default=False)
def __str__(self):
return self.get_username()
Even if you need nothing extra today, defining an empty class User(AbstractUser) at project start is worth it. It costs nothing and leaves the door open for the change you will eventually want.
A Custom User and Manager
Dropping username entirely requires AbstractBaseUser, a custom manager that knows how to create users, and the three class attributes Django's auth machinery reads.
from django.contrib.auth.base_user import BaseUserManager
class UserManager(BaseUserManager):
use_in_migrations = True
def _create_user(self, email, password, **extra_fields):
if not email:
raise ValueError("An email address is required.")
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
# set_password hashes; never assign to user.password directly
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, email, password=None, **extra_fields):
extra_fields.setdefault("is_staff", False)
extra_fields.setdefault("is_superuser", False)
return self._create_user(email, password, **extra_fields)
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)
from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin
from django.db import models
from django.utils import timezone
class User(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(unique=True, db_index=True)
full_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 = UserManager()
# The field used to log in
USERNAME_FIELD = "email"
# Fields prompted for by createsuperuser, excluding USERNAME_FIELD/password
REQUIRED_FIELDS = ["full_name"]
# The field used for password-reset emails
EMAIL_FIELD = "email"
def __str__(self):
return self.email
PermissionsMixin supplies is_superuser, groups and user_permissions. Omit it and the Django admin, along with every permission check, stops working. Also note REQUIRED_FIELDS must not contain USERNAME_FIELD.unique=True on the email is not optional. Authentication looks the user up by USERNAME_FIELD, so a non-unique identifier makes login ambiguous.
How a Login Request Is Resolved
authenticate() walks the configured backends in order, returning the first non-null user. A custom backend is how you support alternatives such as logging in with either an email or a username.
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q
User = get_user_model()
class EmailOrUsernameBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
if username is None:
username = kwargs.get(User.USERNAME_FIELD)
if username is None or password is None:
return None
try:
user = User.objects.get(
Q(email__iexact=username) | Q(username__iexact=username)
)
except User.DoesNotExist:
# Run the hasher anyway so a missing user and a wrong password
# take similar time, avoiding user enumeration by timing.
User().set_password(password)
return None
except User.MultipleObjectsReturned:
return None
if user.check_password(password) and self.user_can_authenticate(user):
return user
return None
AUTHENTICATION_BACKENDS = [
"accounts.backends.EmailOrUsernameBackend",
"django.contrib.auth.backends.ModelBackend",
]
# Argon2 is the recommended hasher; the first entry is used for new passwords
# and the rest allow existing hashes to be verified and upgraded on login.
PASSWORD_HASHERS = [
"django.contrib.auth.hashers.Argon2PasswordHasher",
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
"django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
]
AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
"OPTIONS": {"min_length": 12}},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
pip install django[argon2]. Django upgrades a user's stored hash to the first hasher in the list automatically on their next successful login, so changing the order migrates hashes gradually without a reset.One behaviour worth knowing: user_can_authenticate rejects users with is_active = False. Deactivating an account is therefore the correct way to revoke access — it blocks login while preserving the rows that reference the user.
Wiring the Custom User into Django's Tooling
A custom user needs its own admin class, because the default one references fields that may no longer exist. Login and password-reset views come from django.contrib.auth.urls and work unchanged.
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
from .models import User
@admin.register(User)
class UserAdmin(BaseUserAdmin):
form = UserChangeForm
add_form = UserCreationForm
ordering = ["email"]
list_display = ["email", "full_name", "is_staff", "is_active"]
list_filter = ["is_staff", "is_active", "is_superuser"]
search_fields = ["email", "full_name"]
fieldsets = (
(None, {"fields": ("email", "password")}),
("Profile", {"fields": ("full_name",)}),
("Permissions", {"fields": (
"is_active", "is_staff", "is_superuser", "groups", "user_permissions",
)}),
("Dates", {"fields": ("last_login", "date_joined")}),
)
add_fieldsets = (
(None, {
"classes": ("wide",),
"fields": ("email", "password1", "password2"),
}),
)
from django.urls import path, include
from django.views.generic import CreateView
from django.contrib.auth.forms import UserCreationForm
from django.urls import reverse_lazy
urlpatterns = [
# login, logout, password_change, password_reset and friends
path("accounts/", include("django.contrib.auth.urls")),
path(
"accounts/register/",
CreateView.as_view(
form_class=UserCreationForm,
template_name="registration/register.html",
success_url=reverse_lazy("login"),
),
name="register",
),
]
# settings.py
LOGIN_URL = "login"
LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "login"
• Reference users via settings.AUTH_USER_MODEL in models and get_user_model() elsewhere.
• AbstractUser to add fields; AbstractBaseUser for email-only login.
• Include PermissionsMixin or the admin and all permission checks break.
• USERNAME_FIELD must be unique, and must not appear in REQUIRED_FIELDS.
• Use set_password and check_password; never touch user.password directly.
• Deactivate with is_active = False rather than deleting the row.
Summary
A custom user model is the one piece of Django architecture that is genuinely expensive to defer. Define it in the first commit, even as an empty subclass of AbstractUser, and the option to change identity fields stays open for the life of the project.
Use AbstractUser to extend Django's defaults, or AbstractBaseUser with PermissionsMixin and a custom manager for email-based login. Keep USERNAME_FIELD unique, let the manager own user creation, and leave hashing to set_password.
Everything else — login views, password resets, permissions, the admin — then works on top of the model you defined, because every part of Django's auth stack reads it through AUTH_USER_MODEL rather than assuming its own.