Building a robust authentication system is one of the first challenges you’ll face when developing a Django application. While Django’s default User model works for many simple projects, real‑world apps often require extra fields, alternative login mechanisms, or custom validation rules. That’s where a custom user model shines. In this comprehensive guide we’ll walk you through every step of creating, configuring, and maintaining a custom user model in Python Django, ensuring your project stays scalable, secure, and SEO‑friendly.
Why Use a Custom User Model?
Before diving into the code, it’s worth understanding the benefits that a custom user model brings to a Django project:
- Flexibility: Add fields such as
phone_number,profile_picture, ordate_of_birthwithout resorting to messy one‑to‑one extensions. - Future‑proofing: Changing the user model after the first migration is extremely difficult. Starting with a custom model from day one prevents painful refactors later.
- Alternative authentication: Use email, phone number, or a third‑party identifier as the primary login credential.
- Better integration: Align the user schema with your business logic, analytics, and GDPR compliance requirements.
Step‑by‑Step Guide to Creating a Custom User Model
1. Plan Your Model Structure
Identify the data you need for each user. Common additions include:
email (as username)
first_name, last_name
is_active, is_staff, is_superuser
date_joined
profile_picture
phone_number
Make a quick sketch or a table to avoid missing fields later.
2. Set Up a New Django Project (If Needed)
If you’re starting fresh, run the following commands:
django-admin startproject myproject
cd myproject
python -m venv venv
source venv/bin/activate
pip install django
django-admin startapp accounts
We’ll keep the custom user model inside the accounts app.
3. Define the Custom User Model
Create accounts/models.py and subclass AbstractBaseUser together with PermissionsMixin. This gives you full control over authentication while still supporting Django’s permission framework.
from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin, BaseUserManager
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, **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, password, **extra_fields)
class CustomUser(AbstractBaseUser, PermissionsMixin):
email = models.EmailField('email address', unique=True)
first_name = models.CharField(max_length=30, blank=True)
last_name = models.CharField(max_length=30, blank=True)
phone_number = models.CharField(max_length=20, blank=True)
profile_picture = models.ImageField(upload_to='profiles/', blank=True, null=True)
is_staff = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
date_joined = models.DateTimeField(default=timezone.now)
objects = CustomUserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] # Email & password are required by default
class Meta:
verbose_name = 'user'
verbose_name_plural = 'users'
def __str__(self):
return self.email
4. Register the Model in settings.py
Tell Django to use your new model for authentication:
# myproject/settings.py
INSTALLED_APPS = [
# …
'accounts',
# …
]
AUTH_USER_MODEL = 'accounts.CustomUser'
5. Create and Apply Migrations
Run the migration commands. Because the custom model replaces the built‑in User, you must do this before creating any other apps that reference the default model.
python manage.py makemigrations accounts
python manage.py migrate
6. Update Admin Interface
Without admin registration, you won’t be able to manage users from the Django admin. Add a custom UserAdmin class to handle the new fields.
# accounts/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .models import CustomUser
class CustomUserAdmin(BaseUserAdmin):
list_display = ('email', 'first_name', 'last_name', 'is_staff')
list_filter = ('is_staff', 'is_active', 'is_superuser')
fieldsets = (
(None, {'fields': ('email', 'password')}),
('Personal info', {'fields': ('first_name', 'last_name', 'phone_number', 'profile_picture')}),
('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', 'password1', 'password2', 'is_staff', 'is_active')}
),
)
search_fields = ('email', 'first_name', 'last_name')
ordering = ('email',)
admin.site.register(CustomUser, CustomUserAdmin)
7. Adjust Forms and Authentication Views
If you rely on Django’s built‑in auth views (e.g., PasswordResetView), they already work with AUTH_USER_MODEL. However, custom registration forms need to reference the new model:
# accounts/forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import CustomUser
class CustomUserCreationForm(UserCreationForm):
class Meta(UserCreationForm.Meta):
model = CustomUser
fields = ('email', 'first_name', 'last_name', 'phone_number')
class CustomUserChangeForm(UserChangeForm):
class Meta:
model = CustomUser
fields = ('email', 'first_name', 'last_name', 'phone_number', 'profile_picture')
8. Wire Up URLs and Views
Below is a minimal example using Django’s generic class‑based views for registration and profile editing.
# accounts/views.py
from django.urls import reverse_lazy
from django.views import generic
from .forms import CustomUserCreationForm, CustomUserChangeForm
class SignUpView(generic.CreateView):
form_class = CustomUserCreationForm
success_url = reverse_lazy('login')
template_name = 'registration/signup.html'
class ProfileUpdateView(generic.UpdateView):
form_class = CustomUserChangeForm
success_url = reverse_lazy('profile')
template_name = 'registration/profile.html'
def get_object(self):
return self.request.user
Then add the routes:
# accounts/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('signup/', views.SignUpView.as_view(), name='signup'),
path('profile/', views.ProfileUpdateView.as_view(), name='profile'),
]
Best Practices for Maintaining a Custom User Model
Never Change AUTH_USER_MODEL After Migration
Switching the user model after you have data is a massive undertaking. If you anticipate a future change, consider using a proxy model or adding a OneToOneField profile instead of replacing the core model.
Keep Business Logic Out of the Model
Models should store data and provide simple helpers. Complex validation, email sending, or third‑party API calls belong in services, signals, or form clean methods. This keeps migrations fast and the codebase maintainable.
Use Signals Sparingly
While Django signals (e.g., post_save) can automate profile creation, they add hidden coupling. If you need a guaranteed side effect, prefer overriding save() or using explicit service calls.
Write Unit Tests Early
Testing authentication flows prevents regressions. A minimal test suite might look like:
# accounts/tests.py
from django.test import TestCase
from django.urls import reverse
from .models import CustomUser
class UserModelTests(TestCase):
def test_create_user(self):
user = CustomUser.objects.create_user(email='test@example.com', password='pass123')
self.assertEqual(user.email, 'test@example.com')
self.assertTrue(user.check_password('pass123'))
def test_login_with_email(self):
CustomUser.objects.create_user(email='login@example.com', password='secret')
response = self.client.post(reverse('login'), {
'username': 'login@example.com',
'password': 'secret'
})
self.assertRedirects(response, '/')
Leverage Django’s Built‑In Password Validators
In settings.py enable validators to enforce strong passwords:
AUTH_PASSWORD_VALIDATORS = [
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', '
Leave a Reply