Admin Authentication
Website Rebuild with Django
Chapter 9 · Admin Authentication
django.contrib.auth has been sitting in INSTALLED_APPS since Chapter 1's very first look at settings.py — this chapter is the first time it actually gets used for something. Website Rebuild with Next.js 9 needed a third-party package, NextAuth.js, configured with a custom credentials check to authenticate against the existing site's own bcrypt password hash. Django needs no separate package at all — just one real compatibility problem to solve first.
The Real Problem: Django's Default Hasher Isn't Bcrypt
The existing live site stores its admin password as a bcrypt hash — a string starting $2b$.... Django's own default password hasher is PBKDF2, a different algorithm entirely. Authenticating against the existing hash without forcing an admin password reset means teaching Django to recognize bcrypt specifically, not switching Django's default to it.
Django ships BCryptSHA256PasswordHasher built in — it simply isn't enabled by default. Once listed, Django's own authentication reads a stored hash's own algorithm prefix and picks the matching hasher automatically, regardless of which one appears first in the list.
Importing the Existing Admin — Without Resetting the Password
set_password() takes a plaintext password and hashes it — calling it here would hash the already-hashed string, corrupting it completely. The existing value is already a real bcrypt hash; it belongs directly in .password, unchanged, so Django's own authentication can compare a future login attempt against it exactly as the old PHP site would have.
A Quiet Payoff: The Hash Upgrades Itself
The first time this admin successfully logs in, Django's own authenticate() verifies the submitted password against the stored bcrypt hash — and then, a well-known built-in Django behavior, silently re-hashes that same password using the first hasher in PASSWORD_HASHERS (PBKDF2, Django's preferred one) and saves it. No admin action, no visible step — the stored hash simply becomes a modern Django-native one the moment it's actually used for the first time.
The Login View
LoginView is another built-in — no custom view needed to handle a login form, verify credentials, or start a session; only a template to render it in.
Closing Chapter 8's Own Gap, For Real
One decorator, one line, and Chapter 8's own deliberately-open update_page_title now requires a logged-in staff account before it runs at all — exactly the fix that chapter's own warn-box promised would arrive here.
NextAuth.js vs. Built-In django.contrib.auth
| Next.js | Django | |
|---|---|---|
| Auth package | NextAuth.js — a third-party dependency, added deliberately | django.contrib.auth — already installed since Chapter 1, no new package |
| Checking the existing bcrypt hash | A custom authorize() callback written by hand inside the Credentials provider | A built-in hasher class, enabled by adding it to PASSWORD_HASHERS |
| The login form itself | Built by hand, wired to the Credentials provider | LoginView, built in — only a template needed |
INSTALLED_APPS listing. That's the concrete payoff of the advantage Chapter 1 named honestly up front: Django's batteries-included admin/auth apps meant this chapter's real work was solving one genuine compatibility problem (bcrypt vs. PBKDF2), not building an authentication system from nothing the way a bare framework would have required.
Hands-On Exercises
Explain why BCryptSHA256PasswordHasher had to be added to PASSWORD_HASHERS to authenticate against the existing site's admin password, given that PBKDF2 is Django's own default hasher.
Explain why the existing admin's bcrypt hash is assigned directly to user.password rather than passed through set_password().
Explain what happens to the stored password hash the first time the admin successfully logs in, once both BCryptSHA256PasswordHasher and PBKDF2PasswordHasher are listed in PASSWORD_HASHERS.
Chapter 9 Quick Reference
django.contrib.auth— already installed since Chapter 1; this chapter finally uses it- The real problem: the existing site's password is bcrypt-hashed; Django defaults to PBKDF2
PASSWORD_HASHERS+BCryptSHA256PasswordHasher— built into Django, just not enabled by default; recognizes the existing hash without a password reset- Assign the existing hash directly to
.password— never throughset_password(), which would hash it a second time - Automatic hash upgrade — the first successful login silently re-hashes the password with Django's own preferred (first-listed) hasher
LoginView— built in; only a template is needed@staff_member_required— the real fix for Chapter 8's own deliberately-openupdate_page_titlegap- Next chapter: Admin CRUD Interface