Implementing a Robust Django Custom User Model with AbstractBaseUser
Learn how to design a minimal, secure custom user model in Django using AbstractBaseUser, from requirements through operational checks and failure‑mode mitigation. The guide covers email‑based auth, manager implementation, and when to rethink the design.
16 Sept 2026, 01:17 UTC

Requirements
Before touching any code, list the concrete needs that a custom user model must satisfy for your project:
- Unique login identifier – email, phone, or a custom field.
- Support for password hashing (PBKDF2 or Argon2).
- Ability to add profile fields (avatar, bio) without breaking migrations.
- Compatibility with Django’s admin, auth, and permission systems.
- Clear data boundaries – separate authentication data from application data.
Smallest Suitable Design
The minimal viable custom user model extends AbstractBaseUser and PermissionsMixin, defines a unique identifier, and implements a custom manager. Here’s a skeleton you can copy into accounts/models.py:
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager
from django.db import models
class UserManager(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)
if not extra_fields.get('is_staff'):
raise ValueError('Superuser must have is_staff=True.')
if not extra_fields.get('is_superuser'):
raise ValueError('Superuser must have is_superuser=True.')
return self.create_user(email, password, **extra_fields)
class User(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(unique=True)
first_name = models.CharField(max_length=30, blank=True)
last_name = models.CharField(max_length=30, blank=True)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
date_joined = models.DateTimeField(auto_now_add=True)
objects = UserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
def __str__(self):
return self.email
Key points:
USERNAME_FIELDtells Django which field is used for authentication.- Both
create_userandcreate_superusermust set passwords viaset_passwordto trigger hashing. - Adding profile data can be done inline or via a
OneToOneFieldto a separateProfilemodel.
Configuring the Project
Tell Django to use your model by adding the following to settings.py before running any migrations:
AUTH_USER_MODEL = 'accounts.User'
# Optional: enable Argon2 for stronger hashing
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
]
Do not change AUTH_USER_MODEL after migrations are applied – that will break the ORM.
Trust and Data Boundaries
- The
Usertable holds only authentication‑related data (email, hashed password, flags). - Application‑specific profile data lives in a separate table linked via
OneToOneFieldor separate models, preventing accidental exposure in the admin or API. - All sensitive fields (password) are stored hashed; Django guarantees no plain‑text passwords are ever written.
- Use Django’s built‑in permission system to restrict access to the
Usertable; only superusers can edit raw user records.
Operational Checks
- Initial migration
python manage.py makemigrations accounts python manage.py migrateVerify no migration errors. The first migration should create
accounts_userwith the fields defined. - Superuser creation
python manage.py createsuperuser # Prompt: Email: [contact removed] # Password: ********After creation, log in to
/adminand confirm the user appears with the correct email and that the password authenticates. - Unit test for hashing
from django.test import TestCase from accounts.models import User class UserModelTest(TestCase): def test_password_hashing(self): u = User.objects.create_user(email='[contact removed]', password='secret') self.assertTrue(u.check_password('secret')) self.assertNotEqual(u.password, 'secret')Run
python manage.py test accountsto ensure the hash is stored and verified correctly. - Admin permissions
In the admin, verify that only users with
is_staff=Truecan access the user list. Attempt to edit a user without staff rights and confirm the interface is hidden.
Failure Modes
- Missing
AUTH_USER_MODEL– Django raisesImproperlyConfiguredand ORM references break. - Incorrect manager implementation – if
create_userdoes not hash passwords, the database will store plain text, a critical security flaw. - Running
makemigrationsafter the model is already migrated and then changingAUTH_USER_MODEL– results in migration conflicts and data loss. - Using
AbstractUserinstead ofAbstractBaseUserwhen you need a non‑username identifier can lead to unintended fields (e.g.,usernamemust be unique).
Conditions That Prompt a Redesign
- If you need to switch the primary identifier (e.g., from email to phone) after data exists, you must either migrate data or rebuild the user model from scratch.
- When adding fields that require indexing or unique constraints on large tables, consider a separate profile model to avoid heavy migrations.
- If the application demands multi‑factor authentication or social login, integrate with
django-allauthor similar, which expects the custom user model to expose certain methods. - When scaling beyond a single database instance (e.g., sharding user data), the custom user model may need to be split or replicated, requiring architectural changes.
Practical Checklist
| Step | Command / Action | Expected Result |
|---|---|---|
| Define model before first migration | Place User class in accounts/models.py | No migration errors |
Set AUTH_USER_MODEL | In settings.py | Project recognizes custom user |
| Create migrations | python manage.py makemigrations accounts | accounts_user table created |
| Apply migrations | python manage.py migrate | Database schema updated |
| Test superuser login | Run createsuperuser and log in to admin | Login succeeds; permissions work |
| Verify password hashing | Run unit test above | Hash stored, plain text absent |
Follow this architecture note to implement a secure, maintainable custom user model in Django. By adhering to the minimal design, respecting data boundaries, and performing the outlined checks, you minimize migration headaches and future redesigns.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.