Almost every web application needs users - registering, logging in, managing permissions, and controlling access. Django provides a robust authentication system out of the box. In this chapter, you'll learn how to implement user authentication, permissions, groups, and extend Django's user model for your custom needs.
8.1 Django's Built-in User Model and AbstractUser
Django comes with a default User model that includes common fields: username, password, email, first_name, last_name, is_active, is_staff, is_superuser, and date_joined. However, you often need additional fields like bio, profile picture, or phone number.
# Option 1: Using the default User model (simplest)
from django.contrib.auth.models import User
# Accessing user fields
user = User.objects.create_user(
username='john',
email='john@example.com',
password='securepassword123',
first_name='John',
last_name='Doe'
)
# Check properties
if user.is_authenticated:
print(f"Welcome {user.username}")
if user.is_staff:
print("User can access admin")
if user.is_superuser:
print("User has all permissions")
# Option 2: Extending User with OneToOneField (Profile model)
from django.db import models
from django.contrib.auth.models import User
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
bio = models.TextField(max_length=500, blank=True)
location = models.CharField(max_length=100, blank=True)
birth_date = models.DateField(null=True, blank=True)
profile_picture = models.ImageField(upload_to='profiles/', blank=True)
phone_number = models.CharField(max_length=15, blank=True)
def __str__(self):
return f"{self.user.username}'s profile"
# Access profile
user = User.objects.get(username='john')
profile = user.profile
profile.bio = "Django developer"
profile.save()
# Option 3: Custom User Model using AbstractUser (RECOMMENDED for new projects)
# Create a custom user model from the start - even if you don't need extra fields yet
# models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
class CustomUser(AbstractUser):
# Add custom fields
bio = models.TextField(max_length=500, blank=True)
location = models.CharField(max_length=100, blank=True)
birth_date = models.DateField(null=True, blank=True)
profile_picture = models.ImageField(upload_to='profiles/', blank=True)
phone_number = models.CharField(max_length=15, blank=True)
# Add custom methods
def get_full_name_with_bio(self):
return f"{self.get_full_name()} - {self.bio[:50]}"
def __str__(self):
return self.email # Use email instead of username for display
class Meta:
ordering = ['-date_joined']
# settings.py - Tell Django to use your custom user model
AUTH_USER_MODEL = 'myapp.CustomUser' # 'app_name.ModelName'
# IMPORTANT: Set AUTH_USER_MODEL BEFORE running migrations
# If you already have migrations, you'll need to start over or do a complex migration
Interview Insight: "Why should you use a custom user model from the start?" Even if you don't need extra fields now, changing the user model later is extremely difficult (requires database restructuring). Start with `AbstractUser` for maximum flexibility.
Pro Tip: Always use `create_user()` instead of `create()` - it properly hashes the password. Never store passwords as plain text.
8.2 User Registration, Login, Logout
Implementing authentication views and templates.
# views.py
from django.shortcuts import render, redirect
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from .forms import UserRegistrationForm, UserLoginForm
from .models import CustomUser
def register_view(request):
if request.method == 'POST':
form = UserRegistrationForm(request.POST)
if form.is_valid():
user = form.save(commit=False)
user.set_password(form.cleaned_data['password']) # Hash password
user.save()
# Auto-login after registration
login(request, user)
messages.success(request, 'Registration successful! Welcome!')
return redirect('home')
else:
form = UserRegistrationForm()
return render(request, 'accounts/register.html', {'form': form})
def login_view(request):
if request.method == 'POST':
username = request.POST['username']
password = request.POST['password']
# Authenticate user
user = authenticate(request, username=username, password=password)
if user is not None:
login(request, user)
messages.success(request, f'Welcome back, {user.username}!')
# Redirect to next parameter or home
next_url = request.GET.get('next', 'home')
return redirect(next_url)
else:
messages.error(request, 'Invalid username or password')
return render(request, 'accounts/login.html')
def logout_view(request):
logout(request)
messages.info(request, 'You have been logged out')
return redirect('home')
@login_required
def profile_view(request):
return render(request, 'accounts/profile.html', {'user': request.user})
@login_required
def profile_edit_view(request):
if request.method == 'POST':
user = request.user
user.first_name = request.POST.get('first_name')
user.last_name = request.POST.get('last_name')
user.email = request.POST.get('email')
# Handle profile picture
if request.FILES.get('profile_picture'):
user.profile_picture = request.FILES['profile_picture']
user.save()
messages.success(request, 'Profile updated successfully!')
return redirect('profile')
return render(request, 'accounts/profile_edit.html', {'user': request.user})
# forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm
from .models import CustomUser
class UserRegistrationForm(forms.ModelForm):
password = forms.CharField(widget=forms.PasswordInput, label='Password')
confirm_password = forms.CharField(widget=forms.PasswordInput, label='Confirm Password')
class Meta:
model = CustomUser
fields = ['username', 'email', 'first_name', 'last_name']
def clean_confirm_password(self):
password = self.cleaned_data.get('password')
confirm = self.cleaned_data.get('confirm_password')
if password and confirm and password != confirm:
raise forms.ValidationError("Passwords don't match")
return confirm
def clean_email(self):
email = self.cleaned_data.get('email')
if CustomUser.objects.filter(email=email).exists():
raise forms.ValidationError("Email already registered")
return email
<!-- templates/accounts/register.html -->
{% extends "base.html" %}
{% block content %}
<div class="auth-container">
<h2>Create an Account</h2>
<form method="post">
{% csrf_token %}
{% for field in form %}
<div class="form-group">
{{ field.label_tag }}
{{ field }}
{% if field.errors %}
<div class="error">
{% for error in field.errors %}
<small>{{ error }}</small>
{% endfor %}
</div>
{% endif %}
{% if field.help_text %}
<small class="help">{{ field.help_text }}</small>
{% endif %}
</div>
{% endfor %}
<button type="submit">Register</button>
</form>
<p>Already have an account? <a href="{% url 'login' %}">Login here</a></p>
</div>
{% endblock %}
<!-- templates/accounts/login.html -->
{% extends "base.html" %}
{% block content %}
<div class="auth-container">
<h2>Login to Your Account</h2>
<form method="post">
{% csrf_token %}
<div class="form-group">
<label>Username or Email:</label>
<input type="text" name="username" required>
</div>
<div class="form-group">
<label>Password:</label>
<input type="password" name="password" required>
</div>
<button type="submit">Login</button>
</form>
<p><a href="{% url 'password_reset' %}">Forgot Password?</a></p>
<p>Don't have an account? <a href="{% url 'register' %}">Register here</a></p>
</div>
{% endblock %}
# urls.py - Include Django's built-in auth URLs
from django.urls import path, include
from . import views
urlpatterns = [
path('register/', views.register_view, name='register'),
path('login/', views.login_view, name='login'),
path('logout/', views.logout_view, name='logout'),
path('profile/', views.profile_view, name='profile'),
path('profile/edit/', views.profile_edit_view, name='profile_edit'),
# Django's built-in password reset URLs
path('password-reset/',
include('django.contrib.auth.urls')),
]
# django.contrib.auth.urls provides:
# password_reset/, password_reset/done/, reset/<uidb64>/<token>/, reset/done/
Pro Tip: Use `@login_required` decorator to protect views that require authentication. Add `LOGIN_URL = 'login'` in settings.py to specify where unauthenticated users are redirected.
8.3 Password Reset, Change, and Email Integration
Django provides built-in password reset views that handle token generation and email sending.
# settings.py - Configure email for password reset
# For development - print emails to console
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# For production - use SMTP
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'your-email@gmail.com'
EMAIL_HOST_PASSWORD = 'your-app-password'
DEFAULT_FROM_EMAIL = 'noreply@yourdomain.com'
# Customize password reset email
PASSWORD_RESET_TIMEOUT = 86400 # 24 hours (in seconds)
<!-- templates/registration/password_reset_form.html -->
{% extends "base.html" %}
{% block content %}
<h2>Reset Password</h2>
<p>Enter your email address and we'll send you a link to reset your password.</p>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Send Reset Link</button>
</form>
{% endblock %}
<!-- templates/registration/password_reset_email.html -->
{% autoescape off %}
Hello {{ user.get_username }},
You requested to reset your password for your account at {{ site_name }}.
Click the link below to reset your password:
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
This link will expire in 24 hours.
If you didn't request this, please ignore this email.
Thanks,
The {{ site_name }} Team
{% endautoescape %}
# Custom password change view
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.forms import PasswordChangeForm
@login_required
def change_password_view(request):
if request.method == 'POST':
form = PasswordChangeForm(request.user, request.POST)
if form.is_valid():
user = form.save()
# Important: Update session to prevent logout
update_session_auth_hash(request, user)
messages.success(request, 'Your password was successfully updated!')
return redirect('profile')
else:
messages.error(request, 'Please correct the error below.')
else:
form = PasswordChangeForm(request.user)
return render(request, 'accounts/change_password.html', {'form': form})
Common Mistake: Not calling `update_session_auth_hash()` after password change. Without it, the user gets logged out immediately.
8.4 Permissions: Creating, Assigning, and Checking Permissions
Django has a granular permission system. Each model gets three default permissions: add, change, delete, and view.
# models.py - Custom permissions
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
author = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
is_published = models.BooleanField(default=False)
class Meta:
permissions = [
("can_publish_post", "Can publish/unpublish posts"),
("can_archive_post", "Can archive old posts"),
("can_view_draft", "Can view draft posts"),
]
# Creating permissions programmatically
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
content_type = ContentType.objects.get_for_model(Post)
permission = Permission.objects.create(
codename='can_moderate_comments',
name='Can moderate comments',
content_type=content_type,
)
# Assigning permissions to users and groups
from django.contrib.auth.models import User, Group, Permission
# Assign to individual user
user = CustomUser.objects.get(username='john')
permission = Permission.objects.get(codename='can_publish_post')
user.user_permissions.add(permission)
user.user_permissions.remove(permission)
user.user_permissions.clear()
# Assign to group (better for role-based access)
editors_group, created = Group.objects.get_or_create(name='Editors')
editors_group.permissions.add(permission)
# Add user to group
user.groups.add(editors_group)
# Checking permissions in views
from django.contrib.auth.decorators import permission_required
@permission_required('blog.can_publish_post', raise_exception=True)
def publish_post(request, post_id):
post = Post.objects.get(id=post_id)
post.is_published = True
post.save()
return redirect('post_detail', pk=post_id)
# Checking in templates
{% if perms.blog.can_publish_post %}
<a href="{% url 'publish_post' post.id %}">Publish</a>
{% endif %}
# Checking in code
if request.user.has_perm('blog.can_publish_post'):
# User has permission
pass
# Check if user is in group
if request.user.groups.filter(name='Editors').exists():
pass
Pro Tip: Use groups for role-based access control (RBAC). Create groups like 'Authors', 'Editors', 'Admins' and assign permissions to groups, not individual users.
8.5 Groups and Custom Permission Logic
Complex authorization often requires custom logic beyond simple permissions.
# Custom permission checking in views
from django.core.exceptions import PermissionDenied
def edit_post(request, post_id):
post = Post.objects.get(id=post_id)
# Custom logic: Allow author OR editor OR superuser
if (post.author == request.user or
request.user.has_perm('blog.can_edit_any_post') or
request.user.is_superuser):
# Edit post
pass
else:
raise PermissionDenied("You don't have permission to edit this post")
# Decorator with custom logic
from django.contrib.auth.decorators import user_passes_test
def is_author_or_editor(user, post_id):
from .models import Post
post = Post.objects.get(id=post_id)
return user == post.author or user.has_perm('blog.can_edit_any_post')
@user_passes_test(lambda u: u.is_staff)
def admin_dashboard(request):
return render(request, 'admin_dashboard.html')
# Custom template tag for complex checks
# templatetags/auth_extras.py
from django import template
from blog.models import Post
register = template.Library()
@register.filter
def can_edit(user, post):
return user == post.author or user.has_perm('blog.can_edit_any_post')
# In template
{% if user|can_edit:post %}
<a href="{% url 'edit_post' post.id %}">Edit</a>
{% endif %}
# Class-based view permissions
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin, UserPassesTestMixin
from django.views.generic import UpdateView
class PostUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UserPassesTestMixin, UpdateView):
model = Post
fields = ['title', 'content']
permission_required = 'blog.change_post'
def test_func(self):
post = self.get_object()
return self.request.user == post.author
def handle_no_permission(self):
from django.shortcuts import redirect
return redirect('post_list')
Interview Insight: "What's the difference between permissions and groups?" Permissions are individual actions (can_publish, can_edit). Groups are collections of permissions for role-based access. Use groups to assign multiple permissions at once (e.g., 'Editor' group has publish, edit, delete permissions).
8.6 Social Authentication with django-allauth
Allow users to login with Google, GitHub, Facebook, etc.
# Install
# pip install django-allauth
# settings.py
INSTALLED_APPS = [
'django.contrib.sites',
'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth.socialaccount.providers.google',
'allauth.socialaccount.providers.github',
]
SITE_ID = 1
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
]
# Provider specific settings
SOCIALACCOUNT_PROVIDERS = {
'google': {
'APP': {
'client_id': 'your-google-client-id',
'secret': 'your-google-secret',
'key': ''
},
'SCOPE': ['profile', 'email'],
'AUTH_PARAMS': {'access_type': 'online'},
},
'github': {
'APP': {
'client_id': 'your-github-client-id',
'secret': 'your-github-secret',
}
}
}
LOGIN_REDIRECT_URL = '/'
ACCOUNT_LOGOUT_REDIRECT_URL = '/'
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_AUTHENTICATION_METHOD = 'email'
# urls.py
urlpatterns = [
path('accounts/', include('allauth.urls')),
]
# Template
<a href="{% provider_login_url 'google' %}">Login with Google</a>
<a href="{% provider_login_url 'github' %}">Login with GitHub</a>
8.7 Custom User Models (Extending vs Substituting)
Three ways to customize the user model, each with trade-offs.
# Method 1: Proxy Model (No new database table)
class Organizer(AbstractUser):
class Meta:
proxy = True
permissions = [
("can_manage_events", "Can manage events"),
]
def manage_event(self):
# Custom method
pass
# Method 2: AbstractUser (Add fields to existing user table)
class CustomUser(AbstractUser):
bio = models.TextField(blank=True)
website = models.URLField(blank=True)
# These fields are added to the user table
# Method 3: AbstractBaseUser (Full control, most complex)
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
class CustomUserManager(BaseUserManager):
def create_user(self, email, password=None, **extra_fields):
if not email:
raise ValueError('Email is required')
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save()
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=100)
last_name = models.CharField(max_length=100)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
date_joined = models.DateTimeField(auto_now_add=True)
objects = CustomUserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['first_name', 'last_name']
def get_full_name(self):
return f"{self.first_name} {self.last_name}"
def __str__(self):
return self.email
Recommendation:
- New projects: Use `AbstractUser`
- Existing projects with default User: Use `OneToOneField` Profile model
- Complete custom auth (email login only): Use `AbstractBaseUser`
Authentication Best Practices Checklist
- ✅ Use `AbstractUser` for custom user models in new projects
- ✅ Always hash passwords with `set_password()`
- ✅ Use `@login_required` and `@permission_required` decorators
- ✅ Set `LOGIN_URL` and `LOGIN_REDIRECT_URL` in settings
- ✅ Use HTTPS in production (always!)
- ✅ Implement rate limiting on login attempts
- ✅ Use strong password validation
- ✅ Never log passwords or sensitive data
- ✅ Implement session timeout for sensitive applications
- ✅ Use `update_session_auth_hash()` after password change
Summary
In this chapter, we covered:
- Django's built-in User model and extending with AbstractUser
- User registration, login, and logout implementation
- Password reset and change functionality with email integration
- Permissions system - creating, assigning, and checking permissions
- Groups and custom permission logic for complex authorization
- Social authentication with django-allauth
- Different approaches to custom user models
Your application now has a complete authentication system! In the next chapter, we'll explore Django's powerful admin panel customization.