Hello! Welcome back to our backend engineering course.
In the last lesson, we successfully implemented user registration and login. You saw how Django's UserCreationForm and its associated save() method conveniently handled the creation of new users. We noted that this process securely manages the user's password, but we left the details of how it does this as a question mark.
Today, we're going to erase that question mark. This lesson is a deep dive into the cryptography that underpins secure password management, directly addressing the learning outcome: Explain how Django securely stores passwords using hashing. This is a topic that sits at the intersection of computer science theory and practical security engineering, aligning perfectly with your interest in understanding the "under the hood" mechanics of backend systems.
1. The Principles of Secure Password Storage
Before we look at Django's specific implementation, it's essential to understand the core principles of modern password security. Storing passwords is a high-stakes responsibility, and over the years, the industry has learned (sometimes the hard way) what works and what doesn't.
The worst possible approach is storing passwords in plaintext. If a database is breached, the attacker gets every user's password instantly. A slightly better, but still flawed, approach is encryption. The problem with encryption is that it's reversible; if an attacker steals the encrypted passwords and the decryption key, they can recover the original passwords.
The correct approach is one-way hashing. Let's explore this concept, along with its necessary enhancements.
Password Storage Tier List: encryption, hashing, salting, bcrypt, and beyond
The video 'Password Storage Tier List' provides an excellent conceptual walkthrough, starting from the worst methods and building up to the best practices that modern systems like Django employ.
Please watch the entire video (it's about 8 minutes and 40 seconds). As you watch, focus on understanding the progression of these key concepts: Hashing: Why it's a 'one-way function' or a 'fingerprint'. Rainbow Tables: A common attack against simple hashing. Salting: The technique used to defeat rainbow tables. Slow Hashing & Work Factor: Why deliberately slow algorithms (like bcrypt) are crucial to defend against brute-force attacks.
To summarize the critical ideas from the video:
- Hashing is a process that takes an input (the password) and produces a fixed-size string of characters, called a hash. It's a one-way process, meaning you cannot derive the original password from its hash. During login, you don't decrypt anything; you simply hash the password the user just typed and compare it to the stored hash.
- Salting involves adding a unique, random string (the "salt") to each password before hashing it. This ensures that even if two users have the same password, their stored hashes will be different. This single-handedly defeats rainbow table attacks.
- Key Stretching (Slow Hashing) makes the hashing algorithm intentionally slow by forcing it to run thousands of times. This dramatically increases the cost and time for an attacker to run brute-force or dictionary attacks, making them impractical. The "slowness" is configurable via a work factor (also called iterations or rounds).
2. How Django Stores Passwords
Now that you understand the principles of salting and slow hashing, let's see how Django applies them. Django doesn't reinvent the wheel; it implements these industry-standard best practices out of the box.
The official Django documentation on 'Password management' is the definitive source for this topic. We'll read the key sections that explain the storage format and configuration.
Please read the section 'How Django stores passwords'. Pay close attention to the format of the password string it describes. This is the central piece of the puzzle.
As the documentation explains, a password in a Django User model is not just the hash. It's a single string containing everything needed to verify a password, formatted like this:
algorithm$iterations$salt$hash
Let's break this down:
algorithm: Specifies which hashing algorithm was used (e.g.,pbkdf2_sha256).iterations: This is the work factor. It tells Django how many rounds of hashing to perform.salt: The unique, random salt that was combined with the user's password.hash: The final result of hashing the salted password for the specified number of iterations.
By default, Django uses an algorithm called PBKDF2 with a SHA256 hash. PBKDF2 stands for "Password-Based Key Derivation Function 2," and it's a widely respected standard for key stretching.
When a user logs in, Django:
- Pulls the full password string from the database.
- Parses it to get the algorithm, iterations, and salt.
- Takes the password the user just submitted, adds the same salt, and hashes it using the same algorithm for the same number of iterations.
- Compares the result with the stored hash. If they match, the password is correct.
Test your understanding!
Consider the following (hypothetical) Django password string: pbkdf2_sha256$390000$6dFlA0bma9dU$o5yP5c1soAnz2AOPd61i5l7Z39c4aF2b
Based on this string, what is the work factor (number of iterations) used, and what is the salt?
Show answer
- Work Factor / Iterations:
390000 - Salt:
6dFlA0bma9dU
3. Configuring Password Hashers in Django
Django is flexible. While PBKDF2 is a strong default, you might want to use other modern algorithms like Argon2 (the winner of the 2015 Password Hashing Competition) or bcrypt. You can also adjust the work factor as computing power increases over time.
This is all managed through the PASSWORD_HASHERS setting in your settings.py file.
Let's return to the Django documentation to see how this configuration works and how Django handles upgrades.
Please read the text directly under the heading 'How Django stores passwords' (which you've seen, but re-read for context on PASSWORD_HASHERS) and the section titled 'Password upgrading'.
Here are the key takeaways from that reading:
PASSWORD_HASHERSis a list of supported hashers. The first one in the list is used to hash all new passwords.- When a user logs in, Django can use any of the hashers in the list to verify their password.
- Automatic Upgrading: If a user logs in and their password was hashed with an older algorithm (or a lower work factor), Django automatically re-hashes their password using the preferred (first) hasher and updates the database. This is a powerful feature that transparently improves your application's security over time.
The image below gives a useful comparison of the common password hashing algorithms that Django supports.

4. A Look at the Python Implementation
Given your background, you might find it illuminating to see this process stripped down to pure Python code. It helps to demystify the "magic" and see that it's just a series of well-defined cryptographic operations.
Password hashing in Python with pbkdf2
This article, 'Password hashing in Python with pbkdf2', shows a minimal implementation that follows the exact same pattern as Django.
Read the short introduction and then study the Python code block containing the hash_password and verify_password functions. Notice how hash_password manually constructs the algorithm$iterations$salt$hash string.
The hash_password function in that example is a perfect, simplified model of what Django does. It generates a salt, uses Python's built-in hashlib.pbkdf2_hmac to perform the key stretching, and then formats the output into that familiar string. The verify_password function does the reverse: it parses the string and re-runs the process to check for a match.
Conclusion
You now have a deep understanding of how Django—and modern web applications in general—handle password security. You've moved from seeing it as a black box to understanding the specific cryptographic components and design choices involved.
Key Takeaways:
- Passwords should never be stored in plaintext or encrypted. They must be hashed using a one-way function.
- To be secure against modern attacks, hashing must be salted (to defeat rainbow tables) and slow (to defeat brute-force attacks).
- Django implements these best practices using configurable password hashers, with
PBKDF2as a strong default. - The stored password string (
algorithm$iterations$salt$hash) contains all the information needed to verify a password without ever needing to know the original. - Django can automatically upgrade password hashes to stronger algorithms or higher work factors when users log in.
Preview of the Next Lesson:
We've established how to securely verify a user's identity by checking their password. But once they're authenticated, how does the application remember who they are across multiple requests? In the next lesson, we will explore the two primary strategies for this: session-based and token-based authentication. We will compare their mechanics, use cases, and trade-offs.