Django Basics #7: Django Admin and Built-in Authentication

7 min read

Time to cash in the promise from #1 What is DjangoAdmin comes for free. This post covers the admin page Django auto-generates for you, plus the built-in authentication that ships alongside it (User, login, permissions), in one breath. The final post of the series.

Admin — register a model in one line #

A single line in blog/admin.py.

blog/admin.py
from django.contrib import admin

from .models import Post

admin.site.register(Post)

That’s it. Run runserver and go to http://127.0.0.1:8000/admin/ — but it’s still the login page. You need a superuser.

Creating a superuser #

create superuser
uv run python manage.py createsuperuser

It interactively asks for username / email / password — that’s all. Now you can log in to /admin/ with that account.

After login — Auth (User, Group), Blog (Post) appear in the left menu. Click Post and you see an auto-generated CRUD screen. List, create, edit, delete, and search.

You didn’t write a single extra line of code. This is Django’s most powerful promise.

ModelAdmin — customize the Admin UI #

The default screen is sparse (Post object (1), Post object (2), …). Make it richer with ModelAdmin.

blog/admin.py
from django.contrib import admin

from .models import Post, Tag, Comment


@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    # List view
    list_display = ("title", "author", "is_published", "created_at")
    list_filter = ("is_published", "created_at", "author")
    search_fields = ("title", "content")
    date_hierarchy = "created_at"
    ordering = ("-created_at",)
    list_editable = ("is_published",)
    list_per_page = 25

    # Detail view
    fields = ("title", "author", "content", "tags", "is_published")
    readonly_fields = ("created_at", "updated_at")
    filter_horizontal = ("tags",)        # nicer ManyToMany widget
    autocomplete_fields = ("author",)    # autocomplete for FK with many options
    prepopulated_fields = {"slug": ("title",)}  # auto-fill slug from title


@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
    list_display = ("name", "slug")
    search_fields = ("name",)
    prepopulated_fields = {"slug": ("name",)}


admin.site.register(Comment)

The most common options:

OptionEffect
list_displayColumns to show in the list
list_filterFilters in the right sidebar
search_fieldsTop search box (target fields)
date_hierarchyTop nav drilling down by date
orderingDefault sort
list_editableEdit directly in the list
list_per_pageItems per page
fields / fieldsetsField layout in the detail view
readonly_fieldsRead-only fields
filter_horizontal / filter_verticalTwo-pane widget for M2M
autocomplete_fieldsAutocomplete for FK / M2M (target model needs search_fields)
prepopulated_fieldsAuto-fill from another field (slug is common)

The @admin.register(Post) decorator is equivalent to admin.site.register(Post, PostAdmin). Cleaner.

Inline — edit children alongside the parent #

If you want to show a Post’s Comments right in the post detail view.

Inline
from django.contrib import admin

from .models import Post, Comment


class CommentInline(admin.TabularInline):    # or StackedInline
    model = Comment
    extra = 1                                 # one empty row added
    fields = ("author", "body", "created_at")
    readonly_fields = ("created_at",)


@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ("title", "author")
    inlines = [CommentInline]
  • TabularInline — table style (one row per comment)
  • StackedInline — card style (fields stacked vertically)

Fits places where the parent ↔ child relationship is clear, like cart / order items / post attachments.

Admin’s strengths beyond the surface #

  • Search + filter + pagination — automatic
  • Change history — who changed what, when (Recent Actions)
  • Permission system — per-model add/change/delete/view permissions are auto-generated
  • Localization — UI follows the user’s LANGUAGE_CODE
  • i18n + RTL support

For a small internal tool, Admin alone can serve as your operations UI. Some companies run their entire back-office on it.

Admin security — the production basics #

  • Don’t leave /admin/ at the default — change it (path("private/", admin.site.urls))
  • IP restriction (Nginx or middleware)
  • Two-factor auth (django-otp, django-allauth-2fa)
  • superuser only for those who really need it

Detailed production security is in Advanced #7 Deployment security.

Built-in authentication — django.contrib.auth #

The foundation of Admin is django.contrib.auth. It’s already in INSTALLED_APPS, and migrate already created the User, Group, and Permission tables.

The User model #

using the default User
from django.contrib.auth import get_user_model

User = get_user_model()   # convention — follows settings.AUTH_USER_MODEL

user = User.objects.create_user(
    username="curtis",
    email="me@example.com",
    password="secret123",   # auto-hashed
)

user.set_password("new-password")    # change password
user.save()

user.check_password("new-password")  # → True

create_user and set_password handle password hashing (PBKDF2 by default) automatically. Never store plaintext.

Custom User — recommended from the start #

The default User works, but defining a custom User model from day one is strongly recommended. Switching later is extremely painful.

accounts/models.py — custom User
from django.contrib.auth.models import AbstractUser
from django.db import models


class User(AbstractUser):
    bio = models.TextField(blank=True)
    avatar = models.ImageField(upload_to="avatars/", blank=True, null=True)
config/settings.py
AUTH_USER_MODEL = "accounts.User"

Deeper customization (email login, permissions, etc.) is in Intermediate #4 Users/Permissions.

Login / logout — built-in views #

You don’t have to build them. They’re all in django.contrib.auth.urls.

config/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("accounts/", include("django.contrib.auth.urls")),
    path("blog/", include("blog.urls")),
]

URLs that come with include("django.contrib.auth.urls"):

NamePathRole
login/accounts/login/Login
logout/accounts/logout/Logout
password_change/accounts/password_change/Change password
password_change_done/accounts/password_change/done/Change complete
password_reset/accounts/password_reset/Reset password (via email)
password_reset_done / confirm / completeReset flow

The views are all provided, but you have to build the templates.

templates/registration/login.html
{% extends "base.html" %}

{% block content %}
<h1>Login</h1>
<form method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <button type="submit">Login</button>
</form>
{% if form.errors %}
  <p>Username or password is incorrect.</p>
{% endif %}
<p><a href="{% url 'password_reset' %}">Forgot your password?</a></p>
{% endblock %}

This single file gets login working. logout / password_* go in the same place (templates/registration/).

Redirect after login #

config/settings.py
LOGIN_URL = "/accounts/login/"
LOGIN_REDIRECT_URL = "/blog/"
LOGOUT_REDIRECT_URL = "/blog/"

Sign-up is on you #

The built-ins give you only login / logout / password. Sign-up you build yourself, or use a package like django-allauth.

accounts/views.py — hand-rolled signup
from django.contrib.auth import login
from django.contrib.auth.forms import UserCreationForm
from django.shortcuts import redirect, render


def signup(request):
    if request.method == "POST":
        form = UserCreationForm(request.POST)
        if form.is_valid():
            user = form.save()
            login(request, user)
            return redirect("blog:post_list")
    else:
        form = UserCreationForm()
    return render(request, "registration/signup.html", {"form": form})

UserCreationForm is also built-in — a ModelForm with username + password + password confirmation.

@login_required — login protection #

A one-line decorator on a view function.

blog/views.py
from django.contrib.auth.decorators import login_required


@login_required
def post_new(request):
    ...

If an unauthenticated user hits it, they’re auto-redirected to LOGIN_URL (with the original URL passed as ?next=... so they come back after login).

request.user — the current user #

inside a view
def my_page(request):
    if request.user.is_authenticated:
        username = request.user.username
        ...
in a template
{% if user.is_authenticated %}
  <p>Welcome, {{ user.username }}.</p>
  <a href="{% url 'logout' %}">Logout</a>
{% else %}
  <a href="{% url 'login' %}">Login</a>
{% endif %}

When not logged in, request.user is an AnonymousUser instance (is_authenticated is False). A bare if request.user: is therefore meaningless — always check is_authenticated.

Permissions — @permission_required / user.has_perm #

Django auto-creates four permissions per model — add_<model>, change_<model>, delete_<model>, view_<model>.

permission check
from django.contrib.auth.decorators import permission_required


@permission_required("blog.delete_post", raise_exception=True)
def post_delete(request, post_id):
    ...
directly in code
if request.user.has_perm("blog.change_post"):
    ...
in a template
{% if perms.blog.change_post %}
  <a href="...">Edit</a>
{% endif %}

The production pattern is to bundle permissions into a Group and assign groups to users. The deeper permission model is in Intermediate #4 Users/Permissions.

Recap #

What this post nailed down:

  • admin.site.register(Model) — auto CRUD UI in one line
  • createsuperuser for an admin account
  • ModelAdminlist_display, list_filter, search_fields, date_hierarchy, …
  • Inline — edit children alongside the parent
  • Built-in Usercreate_user, set_password, check_password
  • For new projects, custom User from the start is recommended
  • include("django.contrib.auth.urls") for login/logout/password flows
  • Templates go in templates/registration/ — you build them
  • @login_required, request.user.is_authenticated
  • Permissions — auto-generated per model, @permission_required, user.has_perm, template perms.app.codename
  • Admin is good enough to use as an operations tool — but security (path change, 2FA, IP restriction) is required

Series wrap-up #

The seven posts in one breath:

  1. #1 What is Django — where the full-stack monolith fits
  2. #2 Project setup — uv + startproject + startapp
  3. #3 Models and ORM basics — models, migrations, QuerySet
  4. #4 URL and Views — URLconf, FBV, render
  5. #5 Templates and static files — template inheritance, static
  6. #6 Forms and ModelForm — form validation, ModelForm, file upload
  7. #7 Admin and authentication — auto CRUD, login, permissions ← here

These seven cover all the parts you need to build a small blog. You’re at the level where you can ship a small side project on your own.

The next series, Django Intermediate #1 CBV in depth, covers class-based views, which compress FBV’s repeating patterns. You’ll see how the same CRUD becomes far shorter with five generics — ListView, DetailView, CreateView, UpdateView, DeleteView. From there, Intermediate #2 ORM advanced tackles select_related / prefetch_related, and Intermediate #7 Testing rounds out the flow of taking a project all the way to production.

Thank you for following along. May building your first full-stack app be just the beginning.

X