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.

# settings.py PASSWORD_HASHERS = [ 'django.contrib.auth.hashers.PBKDF2PasswordHasher', # Django's own default — used for any NEW password 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', # recognizes the EXISTING site's bcrypt hash ]

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

from django.contrib.auth.models import User admin = User(username='philip', is_staff=True, is_superuser=True) admin.password = '$2b$12$existingHashFromTheOldSite...' # assigned directly — not hashed again admin.save()
Assigned directly, never passed through set_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

# osztromok_site/urls.py from django.contrib.auth import views as auth_views urlpatterns = [ path('admin-login/', auth_views.LoginView.as_view(template_name='content/login.html'), name='login'), # ... ]

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

from django.contrib.auth.decorators import staff_member_required @staff_member_required def update_page_title(request, full_path): # unchanged from Chapter 8 — now genuinely protected ...

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.jsDjango
Auth packageNextAuth.js — a third-party dependency, added deliberatelydjango.contrib.auth — already installed since Chapter 1, no new package
Checking the existing bcrypt hashA custom authorize() callback written by hand inside the Credentials providerA built-in hasher class, enabled by adding it to PASSWORD_HASHERS
The login form itselfBuilt by hand, wired to the Credentials providerLoginView, built in — only a template needed
The central fact this chapter is built on
Nothing here is "adding" authentication to the project — it's using capability that was already sitting there since Chapter 1's very first 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

Exercise 1

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.

📄 View solution
Exercise 2

Explain why the existing admin's bcrypt hash is assigned directly to user.password rather than passed through set_password().

📄 View solution
Exercise 3

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.

📄 View solution

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 through set_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-open update_page_title gap
  • Next chapter: Admin CRUD Interface