A Quick Way to Customize Authentication Backends, User Manager, User and Permissions in Django

Changing the default username-based authentication of Django to an Email-based authentication

Samandar Komilov
Django Unleashed
6 min readApr 9, 2024

--

Introduction

In the realm of web development, Django stands tall as a robust framework that simplifies the creation of powerful web applications. One of its core functionalities lies in its authentication system, which manages user authentication, permissions, and groups effortlessly. However, there are scenarios where the default setup might not perfectly align with your project’s requirements.

Fear not, for Django offers a flexible solution to tailor authentication mechanisms to suit your needs. By customizing authentication backends, user managers, user models, and permissions, you can wield unparalleled control over user authentication and access control.

In this article, we’ll embark on a journey through Django’s customization capabilities, exploring the creation of a custom authentication backend, user manager, user model, and permissions. Through concise examples and step-by-step guidance, you’ll gain insights into crafting a bespoke authentication system tailored precisely to your project’s specifications.

Building a project: Blog app

  1. Create a django project and an app:
django-admin startproject blog_project .
python manage.py startapp users

2. Define a Custom User Manager in users/models.py :

from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin, Group as BaseGroup
from django.db import models


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)

First of all, we should get the meaning behind User Managers in Django.

👉 User Managers are classes that handle the creation, retrieval, updating, and deletion of user objects. They act as intermediaries between your Django application and the database, providing a layer of abstraction for interacting with user data.

By default, Django provides a UserManager class that handles the creation and management of user objects for the built-in User model. However, you can create your own custom UserManager to add additional functionality or modify the default behavior.

User managers typically provide methods for tasks such as creating new users, retrieving users by various criteria (e.g., username, email), updating user information, and deleting users. They may also include methods for handling authentication-related tasks, such as verifying passwords or creating tokens for password reset functionality.

✅ In the above code, we are extending the functionality of Basic User Manager to that can work with emails instead of usernames:

  • create_user() — creates a user by checking the email field against emptiness, normalizing it and setting a password. Returns a user object.
  • create_superuser() — creates a superuser by defining 2 vital extra fields as True — staff status and superuser status of the user. If any of these fields are False, then the user is not a superuser, hence this function raises an error. Otherwise, creates a superuser.

3. Create a Custom User Model in users/models.py again:

class CustomUser(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(unique=True)
first_name = models.CharField(max_length=255, blank=True)
last_name = models.CharField(max_length=255, blank=True)
about = models.TextField(blank=True)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
is_superuser = models.BooleanField(default=False)

objects = CustomUserManager()

USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []

def __str__(self):
return self.email

def has_perm(self, perm, obj=None):
return self.is_staff

def has_module_perms(self, app_label):
return self.is_staff

✅ Here, we are not extending the existing User Model of Django, instead we are creating a new User Model from scratch:

  • objects — defines which User Manager manages these User objects.
  • USERNAME_FIELD = ‘email’ — specifies the field used as the unique identifier for a user when logging in. In this case, it’s the email field.
  • REQUIRED_FIELDS = [] — specifies any additional required fields when creating a user.
  • has_perm() method determines if the user has a specific permission (perm) within the system. It returns True if the user is a staff member (is_staff is True), otherwise False.
  • has_module_perms() method determines if the user has permissions to view the app with the given app_label. It returns True if the user is a staff member (is_staff is True), otherwise False.

4. Create a Custom Group Model in users/models.py (optional):

class CustomGroup(BaseGroup):
class Meta:
proxy = False

def __str__(self):
return self.name

Creating a Custom Group Model is not mandatory actually. Because, unless you even want to add custom fields other than name and permissions to the model (very rare), there is no point to change the default model. But anyway, we decided to consider this case too:

  • You can add new fields as you wish, but here we didn’t do that.
  • proxy = False — this line ensures that the table for a CustomGroup model will not be created, it stays being as proxy model.

5. Create a Custom Authentication Backend in users/backends.py by overriding ModelBackend:

from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend


class EmailBackend(ModelBackend):
def authenticate(self, request, email=None, password=None, **kwargs):
user_model = get_user_model()
try:
user = user_model.objects.get(email=email)
if user.check_password(password):
return user
except user_model.DoesNotExist:
return None

❓ Well, what is an authentication backend actually?

👉 Authentication Backend is a Python class responsible for authenticating users. While Django comes with a default authentication backend that authenticates users against the database-backed user model, you might encounter scenarios where you need to customize the authentication process.

Authentication backends allow you to integrate alternative authentication methods or authenticate against different sources such as LDAP, OAuth, or external APIs. They provide a way to extend Django’s authentication system beyond the default behavior.

❗️ Again, the primary purpose of an authentication backend is to verify the identity of users attempting to log in to the Django application. This involves validating credentials provided by the user, such as username and password, against the chosen authentication source.

In the above code, we are overriding the default ModelBackend and changing the behaviour of authenticate() method:

  • instead of username field, we are checking for email field.

6. Configure settings.py file:

# Customization

AUTH_USER_MODEL = 'users.CustomUser'
AUTH_GROUP_MODEL = 'users.CustomGroup'

AUTHENTICATION_BACKENDS = [
'users.auth_backends.EmailBackend',
'django.contrib.auth.backends.ModelBackend',
]

After creating Custom User and Custom Group Models, we have to specify the AUTH_USER_MODEL and AUTH_GROUP_MODEL settings in settings.py. AUTHENTICATION_BACKENDS also have to be changed as shown.

7. Create a new app called blog and configure the basic models and views:

# blog/models.py

from django.db import models
from django.contrib.auth.models import User # Use custom user model

class Post(models.Model):
title = models.CharField(max_length=255)
content = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

def __str__(self):
return self.title
from django.shortcuts import render, redirect
from django.contrib.auth import authenticate, login, logout
from .models import Post
from .forms import CreatePostForm

def post_list(request):
posts = Post.objects.all().order_by('-created_at')
return render(request, 'blog/post_list.html', {'posts': posts})

def create_post(request):
if request.method == 'POST':
form = CreatePostForm(request.POST)
if form.is_valid():
form.save(commit=False).author = request.user # Associate post with user
form.save()
return redirect('post_list')
else:
form = CreatePostForm()

This is just a test, you can configure your project as you want. For this reason, we didn’t consider urls, templates and other staff thoroughly.

8. Create custom permissions for users in users/permissions.py:

from django.contrib.auth.models import Permission

class CanEditPost(Permission):
name = 'Can edit any post'
codename = 'can_edit_post'

class CanPublishPost(Permission):
name = 'Can publish own post'
codename = 'can_publish_post'

❓ What are permissions and why we need them?

👉 Permissions are a crucial component of the authorization system, determining what actions users can perform within the application. They govern access to views, models, and other resources based on the roles and privileges assigned to individual users or groups.

You can see default permissions that a User can have while creating it in admin panel:

9. Modify the Custom User Model to include the permissions we created just now:

class CustomUser(AbstractBaseUser):
# ... (existing code)

class Meta:
permissions = [
('can_edit_post', 'Can edit any post'),
('can_publish_post', 'Can publish own post'),
]

objects = UserManager()

# ... (existing code)

After creating custom permissions, we have to specify that permissions in our Custom User Model to make them work.

Conclusion

Well, actually this is not a complete project. Actually, my goal was not doing that as well, rather I wanted to show how to create a “blueprint” for working with customized authentication elements of Django. I hope that goal has been achieved and if so I’m really happy. Provide feedbacks if you encounter something wrong or misleading. Thanks for your attention.

--

--