🔐 Django Authentication System
Almost every real app needs to answer two questions: who is this? and what are they allowed to do? Django ships a battle-tested answer to both — secure password hashing, sessions, users, groups, and permissions — so you build features instead of reinventing security.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Distinguish authentication (who you are) from authorization (what you may do)
- Create users safely and explain why to define a custom user model from day one
- Wire up login, logout, and registration using Django's built-in views and forms
- Protect views with
@login_required,permission_required, and CBV mixins - Model roles with groups and permissions, and enforce object-level ownership
- Apply core security settings for passwords, sessions, and HTTPS
Estimated Time: 55–70 minutes • Difficulty: Intermediate
Hands-on: Add registration, login, and a login-protected profile page to a small app.
In This Lesson
Authentication vs. Authorization
These two words are easy to blur, but the distinction runs through everything in this lesson:
- Authentication proves identity — "you are who you claim to be." Logging in with a password is authentication.
- Authorization checks permission — "you're allowed to do this." Deciding whether you can delete an article is authorization.
💡 The office-building analogy: Authentication is showing your badge at the front desk to get inside. Authorization is whether that badge opens the server-room door. You can be authenticated (inside the building) yet not authorized (locked out of a room).
📖 The pieces Django gives you
User & Group models for identities and roles; permissions for granular rights; secure password hashing; and cookie-backed sessions that remember a logged-in user between requests.
The User Model
The default User model stores a username, hashed password, email, names, the is_active/is_staff/is_superuser flags, and join/login timestamps.
Creating users the safe way
from django.contrib.auth import get_user_model, authenticate
User = get_user_model()
# ALWAYS use create_user — it hashes the password. Never User.objects.create().
user = User.objects.create_user(
username="ada",
email="ada@example.com",
password="a-strong-passphrase",
)
# Verifying credentials returns the user or None
who = authenticate(username="ada", password="a-strong-passphrase")
if who is not None:
... # credentials good
⚠️ Never store or set a raw password
create_user() and set_password() hash the password (PBKDF2-SHA256 by default). User.objects.create(password="...") stores it in plain text — a serious breach waiting to happen. And always call get_user_model() instead of importing User directly, so your code works with a custom user model.
Define a custom user model on day one
Django strongly recommends a custom user model for every new project — even one that starts identical to the default. Swapping it later, once tables and foreign keys exist, is painful.
💡 Buy the roomier suit: A custom user model is like buying a suit with a little extra room. You may not need the space today, but altering it later is far cheaper than replacing the whole thing after your database is full of related rows.
# accounts/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
bio = models.TextField(blank=True)
birth_date = models.DateField(null=True, blank=True)
# settings.py — set this BEFORE the first migration
AUTH_USER_MODEL = "accounts.User"
Logging in with email instead of username
To drop usernames entirely, subclass AbstractUser, remove username, make email the identifier, and supply a manager:
# accounts/models.py
from django.contrib.auth.models import AbstractUser, BaseUserManager
from django.db import models
class UserManager(BaseUserManager):
def create_user(self, email, password=None, **extra):
if not email:
raise ValueError("Users must have an email address.")
user = self.model(email=self.normalize_email(email), **extra)
user.set_password(password) # hashes it
user.save(using=self._db)
return user
def create_superuser(self, email, password=None, **extra):
extra.setdefault("is_staff", True)
extra.setdefault("is_superuser", True)
return self.create_user(email, password, **extra)
class User(AbstractUser):
username = None
email = models.EmailField("email address", unique=True)
USERNAME_FIELD = "email"
REQUIRED_FIELDS = [] # email & password are prompted automatically
objects = UserManager()
Login, Logout & Registration
Django ships class-based views for login, logout, and the whole password-reset flow. You supply templates; Django supplies the logic.
Built-in auth views
# accounts/urls.py
from django.contrib.auth import views as auth_views
from django.urls import path
urlpatterns = [
path("login/", auth_views.LoginView.as_view(template_name="accounts/login.html"), name="login"),
path("logout/", auth_views.LogoutView.as_view(), name="logout"),
# Forgot-password flow (needs email configured)
path("password-reset/", auth_views.PasswordResetView.as_view(), name="password_reset"),
path("password-reset/done/", auth_views.PasswordResetDoneView.as_view(), name="password_reset_done"),
path("reset/<uidb64>/<token>/", auth_views.PasswordResetConfirmView.as_view(), name="password_reset_confirm"),
path("reset/done/", auth_views.PasswordResetCompleteView.as_view(), name="password_reset_complete"),
]
💡 Logout must be POST
Since Django 5, LogoutView only accepts POST (a GET link could be triggered by an image tag — a CSRF risk). Render a tiny form: <form method="post" action="{% url 'logout' %}">{% csrf_token %}<button>Log out</button></form>. Set LOGIN_REDIRECT_URL and LOGOUT_REDIRECT_URL in settings to control where users land.
Registration
Django has no built-in signup view, but UserCreationForm does the heavy lifting — including running the password validators:
# accounts/forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import get_user_model
User = get_user_model()
class RegisterForm(UserCreationForm):
email = forms.EmailField(required=True)
class Meta:
model = User
fields = ["username", "email", "password1", "password2"]
# accounts/views.py
from django.contrib.auth import login
from django.shortcuts import render, redirect
from .forms import RegisterForm
def register(request):
form = RegisterForm(request.POST or None)
if request.method == "POST" and form.is_valid():
user = form.save()
login(request, user) # log them in right away
return redirect("home")
return render(request, "accounts/register.html", {"form": form})
Sessions
HTTP is stateless — each request arrives with no memory of the last. Sessions bridge that gap: on login, Django stores session data server-side and hands the browser a signed cookie holding only the session key. Every later request presents that cookie, and Django re-loads who's logged in.
Reading and writing session data
# A guest shopping cart, kept in the session
def add_to_cart(request, product_id):
cart = request.session.setdefault("cart", {})
cart[str(product_id)] = cart.get(str(product_id), 0) + 1
request.session.modified = True # required when mutating nested data
return redirect("cart")
⚠️ Mutating nested session data needs a nudge
Django auto-saves the session when you assign a key directly (request.session["x"] = 1). But when you mutate a dict or list inside the session, it can't tell — set request.session.modified = True so the change is persisted.
Permissions & Protecting Views
For every model, Django auto-creates four permissions: add_, change_, delete_, and view_. You can add your own in the model's Meta:
class Article(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
class Meta:
permissions = [
("publish_article", "Can publish articles"),
]
Protecting function-based views
from django.contrib.auth.decorators import login_required, permission_required
@login_required
def profile(request):
return render(request, "accounts/profile.html")
@permission_required("blog.publish_article", raise_exception=True)
def publish(request, pk):
article = get_object_or_404(Article, pk=pk)
article.status = "published"
article.save()
return redirect("article_detail", pk=pk)
💡 raise_exception=True
By default permission_required redirects a logged-in-but-unauthorized user to the login page — confusing, since they're already logged in. Passing raise_exception=True returns a proper 403 Forbidden instead.
Protecting class-based views
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.views.generic import UpdateView
class ArticleUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
model = Article
fields = ["title", "content"]
permission_required = "blog.change_article"
raise_exception = True
Checking permissions in templates
{% if perms.blog.publish_article %}
<a href="{% url 'publish' article.pk %}">Publish</a>
{% endif %}
{% if user.is_authenticated %}
<span>Hi, {{ user.get_username }}</span>
{% else %}
<a href="{% url 'login' %}">Log in</a>
{% endif %}
Groups & Roles
Assigning permissions to individual users doesn't scale. Instead, bundle permissions into groups and put users in groups — this is role-based access control (RBAC).
from django.contrib.auth.models import Group, Permission
editors, _ = Group.objects.get_or_create(name="Editors")
publish = Permission.objects.get(codename="publish_article")
editors.permissions.add(publish)
user.groups.add(editors) # user now inherits "publish_article"
if user.groups.filter(name="Editors").exists():
... # user is an editor
Real example: a news site with Writers (edit their own drafts), Editors (edit and publish anything), and Admins (manage users). Groups and their permissions are also fully manageable through the Django admin.
Object-Level Access
Model permissions are all-or-nothing: "can change any article." Often you want "can change my own article." That's object-level access, and the simplest approach is an explicit ownership check.
from django.core.exceptions import PermissionDenied
@login_required
def edit_article(request, pk):
article = get_object_or_404(Article, pk=pk)
if article.author != request.user:
raise PermissionDenied # → 403
# ... proceed with the edit
In class-based views, filter the queryset so users simply can't reach rows they don't own (a missing row yields a clean 404):
class ArticleUpdateView(LoginRequiredMixin, UpdateView):
model = Article
fields = ["title", "content"]
def get_queryset(self):
return Article.objects.filter(author=self.request.user)
💡 When to reach for a package
For complex per-object rules across many users (sharing, teams, granular grants), a library like django-guardian stores per-object permissions in the database. For the common "owner-only" case, the checks above are simpler and faster — don't add the dependency until you need it.
Security Best Practices
Enforce strong passwords
# settings.py
AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
"OPTIONS": {"min_length": 10}},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
Secure cookies and HTTPS (production)
# settings.py — production
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True # cookies only over HTTPS
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True # JS can't read the session cookie
SECURE_HSTS_SECONDS = 31_536_000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
✅ Security checklist
- Always create/change passwords via
create_user()/set_password(). - Keep
SECRET_KEYout of source control (use environment variables). - Throttle login attempts (e.g. django-axes) to blunt brute-force attacks.
- Consider two-factor auth for privileged accounts.
- Run
python manage.py check --deploybefore shipping.
⚠️ Client-side validation is not security
Anything checked only in the browser can be bypassed with a crafted request. Every permission and validation rule that matters must be enforced on the server.
Hands-on Exercise
🏋️ Add accounts to a small app
Objective: Give an existing app registration, login/logout, and a profile page only logged-in users can see.
Requirements
- A
registerview usingUserCreationFormthat logs the new user in on success. - Login and logout wired to Django's built-in views (remember: logout is POST).
- A
profileview decorated with@login_required. LOGIN_REDIRECT_URLandLOGIN_URLset in settings.
💡 Hint
The unauthenticated redirect for @login_required comes from LOGIN_URL (or the login URL name). After login, Django honours a ?next= query param, falling back to LOGIN_REDIRECT_URL.
✅ Sample solution
# accounts/views.py
from django.contrib.auth import login
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import UserCreationForm
from django.shortcuts import render, redirect
def register(request):
form = UserCreationForm(request.POST or None)
if request.method == "POST" and form.is_valid():
user = form.save()
login(request, user)
return redirect("profile")
return render(request, "accounts/register.html", {"form": form})
@login_required
def profile(request):
return render(request, "accounts/profile.html")
# accounts/urls.py
from django.contrib.auth import views as auth_views
from django.urls import path
from . import views
urlpatterns = [
path("register/", views.register, name="register"),
path("profile/", views.profile, name="profile"),
path("login/", auth_views.LoginView.as_view(
template_name="accounts/login.html"), name="login"),
path("logout/", auth_views.LogoutView.as_view(), name="logout"),
]
# settings.py
LOGIN_URL = "login"
LOGIN_REDIRECT_URL = "profile"
LOGOUT_REDIRECT_URL = "login"
Summary & Quiz
🎉 Key Takeaways
- Authentication proves identity; authorization checks permission — keep them distinct.
- Create users with
create_user()(it hashes passwords); reference the model viaget_user_model(). - Define a custom user model before your first migration — changing it later is painful.
- Django's built-in views cover login, logout, and password reset; you just add templates.
- Protect views with
@login_required,permission_required, and CBV mixins; model roles with groups. - Enforce ownership for object-level access, and lock down passwords, cookies, and HTTPS in production.
🎯 Quick Quiz
Question 1: Which method creates a user with a properly hashed password?
Question 2: You want a logged-in but unauthorized user to get a 403 rather than being bounced to the login page. What do you add?
Question 3: What is the recommended way to manage permissions for many users with the same role?
📚 Further Reading
- Django docs — Using the authentication system
- Django docs — Customizing authentication (custom user model)
- Django docs — Password management
- OWASP Top 10 — common security risks
🚀 What's Next?
You've covered Django's server-rendered core. Next we shift toward APIs: building RESTful services with Flask-RESTful, where authentication and validation reappear in a lighter, API-first framework.
🎉 Excellent work!
You can now sign users in, keep them logged in, and control what they may do. On to REST APIs.