Python Django Authentication Permissions and Custom Users
python django authentication permissions and custom users: Implement custom user models and permission systems in Django. This guide covers authentication, groups, and...
When you start a new Django project, the default User model works for basic cases, but most real applications need a custom user model to store additional fields or change the authentication identifier. This article explains how to set up python django authentication permissions and custom users, covering the model, permissions, groups, and the security implications of each choice.
Why a Custom User Model Is the Right Starting Point
Django's built-in User model uses a username and password by default. If your application needs email-based login, a phone number, or extra profile fields, you have two options: extend the default model with a OneToOneField or replace it with a custom model. The latter is almost always the better choice because Django's authentication framework is tightly coupled to the user model. Changing it after the first migration is painful, but doing it at project start is straightforward.
A custom user model also lets you control how permissions are stored and which fields are used for authentication. For example, you can set USERNAME_FIELD = 'email' to use email as the login identifier. This is a common requirement for modern web applications.
Setting Up a Custom User Model in Django
Create a new Django project and app, then define a custom user model. The model should inherit from AbstractBaseUser and PermissionsMixin to get password hashing and permission methods. You also need a custom manager that handles user creation.
# accounts/models.py from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin 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) return self.create_user(email, password, **extra_fields) class CustomUser(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) objects = CustomUserManager() USERNAME_FIELD = 'email' REQUIRED_FIELDS = [] def __str__(self): return self.email
Update settings.py to point to this model:
# settings.py AUTH_USER_MODEL = 'accounts.CustomUser'
Run makemigrations and migrate before creating any other models that reference the user. This ensures the custom model is used in foreign keys and many-to-many relationships.
Configuring Authentication Backends and Login
Django's default authentication backend works with the USERNAME_FIELD you defined. Since we set it to email, the login form must use email instead of username. You can use Django's built-in AuthenticationForm by overriding the username field label, or create a custom form.
# accounts/forms.py from django.contrib.auth.forms import AuthenticationForm class EmailAuthenticationForm(AuthenticationForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['username'].label = 'Email'
In views, use authenticate() and login() as usual. Django will call the backend with the email and password, and the backend will look up the user by the USERNAME_FIELD.
# accounts/views.py from django.contrib.auth import authenticate, login from django.shortcuts import redirect, render from .forms import EmailAuthenticationForm def login_view(request): if request.method == 'POST': form = EmailAuthenticationForm(request, data=request.POST) if form.is_valid(): email = form.cleaned_data['username'] password = form.cleaned_data['password'] user = authenticate(request, username=email, password=password) if user is not None: login(request, user) return redirect('home') else: form = EmailAuthenticationForm() return render(request, 'registration/login.html', {'form': form})
Assigning and Checking Permissions
Django's permission system is model-based. Each model can have add, change, delete, and view permissions. You can also define custom permissions in a model's Meta class.
# accounts/models.py class Project(models.Model): name = models.CharField(max_length=100) owner = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='projects' ) class Meta: permissions = [ ('can_archive_project', 'Can archive a project'), ]
After migrating, these permissions are created in the database. You can assign them to users or groups programmatically:
from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType content_type = ContentType.objects.get_for_model(Project) permission = Permission.objects.get( codename='can_archive_project', content_type=content_type, ) user.user_permissions.add(permission)
In views, check permissions with request.user.has_perm('accounts.can_archive_project') or use the @permission_required decorator.
from django.contrib.auth.decorators import permission_required @permission_required('accounts.can_archive_project') def archive_project(request, pk): # ...
Using Groups for Role-Based Access
Groups let you bundle permissions and assign them to many users at once. This is the standard way to implement roles like "editor" or "admin".
from django.contrib.auth.models import Group, Permission editor_group = Group.objects.create(name='Editors') permission = Permission.objects.get(codename='change_project') editor_group.permissions.add(permission) user.groups.add(editor_group)
When you check a permission, Django first checks the user's direct permissions, then the permissions of all groups the user belongs to. This makes groups a convenient layer for role-based access control.
Managing Permissions in the Admin and Code
The Django admin automatically lists model permissions if you register the model. For custom permissions, they appear in the user and group admin pages. You can also manage permissions programmatically in data migrations to ensure they exist in production.
# accounts/migrations/0002_add_project_permissions.py from django.db import migrations def add_permissions(apps, schema_editor): Permission = apps.get_model('auth', 'Permission') ContentType = apps.get_model('contenttypes', 'ContentType') Project = apps.get_model('accounts', 'Project') content_type = ContentType.objects.get_for_model(Project) Permission.objects.get_or_create( codename='can_archive_project', content_type=content_type, defaults={'name': 'Can archive a project'}, ) class Migration(migrations.Migration): dependencies = [ ('accounts', '0001_initial'), ] operations = [ migrations.RunPython(add_permissions), ]
Security Considerations for Custom Users and Permissions
Changing AUTH_USER_MODEL after running initial migrations is extremely difficult. Always set it before the first migration. Also, avoid using User directly in foreign keys; use settings.AUTH_USER_MODEL to keep references dynamic.
Permission checks should be performed server-side. Never rely on hiding UI elements as a security measure. Use Django's permission decorators or mixins in class-based views, and always test that unauthorized users receive a 403 response.
Another common pitfall is forgetting to set is_staff for users who need admin access. The admin site requires is_staff=True, but is_superuser is separate. If you use custom permissions, make sure your create_superuser method sets both flags correctly.
Finally, when you add custom permissions to a model, they are not automatically created for existing databases unless you run a migration that includes them. Use data migrations to add permissions in production environments, as shown above.
Handling Permission Checks in Views and Templates
In templates, you can check permissions with the perms variable. This is useful for conditionally showing actions.
{% if perms.accounts.can_archive_project %} <a href="{% url 'archive_project' project.id %}">Archive</a> {% endif %}
For class-based views, use the PermissionRequiredMixin:
from django.contrib.auth.mixins import PermissionRequiredMixin from django.views.generic import UpdateView class ProjectUpdateView(PermissionRequiredMixin, UpdateView): permission_required = 'accounts.change_project' model = Project fields = ['name']
The mixin redirects unauthenticated users to the login page and returns a 403 for authenticated users without the permission. This behavior is consistent with the decorator and keeps access control centralized in the view definition.