Hello! In our last lesson, we learned how to trace a request through the entire backend stack using tools like the Django Debug Toolbar and strace. This gave us a powerful toolkit for diagnosing performance issues.
Now, we'll shift our focus from performance to one of the most critical aspects of backend security: authentication. We'll begin by tackling the fundamental question of how to handle user passwords securely.
This lesson directly addresses the learning outcome: Explain how password hashing works using a slow algorithm like bcrypt, including the role of salts.
We will cover:
- Why you must never store passwords in plaintext.
- The difference between encryption and hashing, and why hashing is the correct approach.
- How salts protect against large-scale attacks like rainbow tables.
- Why modern password security relies on deliberately slow hashing algorithms like bcrypt.
- How to configure and use bcrypt correctly in practice.
Let's begin by establishing the golden rule of password management.
The Cardinal Sin: Storing Plaintext Passwords
It might seem obvious, but the number one rule is to never, ever store a user's password in a recoverable format. Storing passwords as plain text in your database is a catastrophic liability. If an attacker breaches your database, they instantly have the credentials for every single one of your users.
A common first thought might be, "Okay, I'll just encrypt the passwords." However, this is also incorrect.
Password Hashing Guide 2025: Argon2 vs Bcrypt vs Scrypt vs ...
To understand why encryption is the wrong tool for this job, let's read a short section from a comprehensive guide on password hashing. This will clarify the crucial difference between a two-way process (encryption) and a one-way process (hashing).
Read the section 'Hashing vs. Encryption: A Critical Distinction'. It's near the beginning of the article.
As the article states, encryption is reversible. If an attacker steals your database and your encryption key, they can decrypt all the passwords. The goal is not to hide the password, but to make it impossible to recover, while still being able to verify it. This is where hashing comes in.
Hashing: The One-Way Street
Hashing is a process that takes an input (like a password) and produces a fixed-size string of characters, called a hash. This process is designed to be a one-way function. You can easily compute the hash from the password, but you cannot compute the password from the hash.
Password Storage Tier List: encryption, hashing, salting, bcrypt, and beyond
The video 'Password Storage Tier List' provides an excellent, clear introduction to the concept of hashing for passwords.
Watch from 01:40 to 04:15. This segment explains what hashing is, why it's a one-way function using the 'paint mixing' analogy, and why older, fast hashing algorithms like MD5 and SHA-1 are no longer secure for this purpose.
So, the process is:
- Sign-up: Hash the user's password and store the hash in the database.
- Login: Hash the password the user enters and compare it to the stored hash. If they match, the password is correct.
However, using a simple, fast hash function like SHA-256 is still dangerously insecure for passwords. Why? Because an attacker who steals your database can still crack the passwords.
The Problem with Fast Hashes and the Solution: Salting
Fast hash functions are vulnerable to two main types of attacks:
- Dictionary Attack: The attacker takes a huge list of common passwords, hashes them all, and sees if any match the hashes in your stolen database.
- Rainbow Table Attack: This is a pre-computed dictionary attack. Attackers have massive tables that map pre-computed hashes back to the passwords that created them.
The video below demonstrates just how easy it is to "reverse" a hash from an old algorithm like MD5 using these techniques.
Hashing Passwords in Node and Express using bcrypt
This video, 'Hashing Passwords in Node and Express using bcrypt,' starts with a powerful demonstration of why algorithms like MD5 are broken for password storage.
Watch from 02:14 to 04:04. You'll see how a password hashed with MD5 can be found in seconds just by searching for the hash online.
The core problem is that for a given hashing algorithm, the same password always produces the same hash. This allows for these pre-computation attacks. The solution is to make every hash unique, even for the same password. We do this with a salt.
A salt is a unique, random string of data that is generated for each user and combined with their password before hashing.
HashedPassword = HashFunction(Password + Salt)
This simple addition has two profound effects:
- It renders rainbow tables useless because the hashes are no longer pre-computable. An attacker would need a separate rainbow table for every possible salt, which is computationally impossible.
- It ensures that if two users have the same password, their stored hashes will be different because they will have different salts.
Now, let's see how salting works in practice.
Password Storage Tier List: encryption, hashing, salting, bcrypt, and beyond
Let's return to the 'Password Storage Tier List' video to see a clear explanation of salting.
Watch the section on salting from 05:15 to 06:37. It explains how a salt is generated, used, and stored.
The salt is not a secret; it's stored right alongside the user's hashed password in the database. When the user logs in, you retrieve both their salt and their stored hash. You then combine the password they entered with the retrieved salt and hash it. If the result matches the stored hash, the login is successful.
The Final Piece: Deliberately Slow Hashing with bcrypt
Salting protects against mass, pre-computed attacks. But what if an attacker gets your database and decides to focus all their computing power on cracking a single user's password? They can still perform a dictionary attack on that one hash. With modern GPUs, they can try billions of passwords per second.
This is where the most important innovation in modern password hashing comes in: deliberate slowness.
Algorithms like bcrypt, scrypt, and Argon2 are designed to be computationally expensive. They introduce an adjustable work factor (also called a cost factor or salt rounds) that forces the CPU to perform thousands of iterations to compute a single hash.

This slowness creates a massive asymmetry:
- For a legitimate user: Waiting 200 milliseconds for a single hash during login is unnoticeable.
- For an attacker: The same 200ms delay reduces their billion-guesses-per-second attack to just 5 guesses per second, making a brute-force attack completely impractical.
Password Storage Tier List: encryption, hashing, salting, bcrypt, and beyond
The 'Password Storage Tier List' video explains this concept of slow hashing very well.
Watch from 06:46 to 08:40. This section covers why we need specialized, slow password hashing functions like bcrypt and explains the concept of a 'work factor'.
The work factor is adaptive. As computers get faster over the next few years, you can simply increase the work factor to maintain the same level of security, without changing the algorithm itself.
A Deeper Look at bcrypt
Since you're interested in the deep technical details, let's explore how bcrypt achieves this.
Hashing in Action: Understanding bcrypt
The 'Hashing in Action: Understanding bcrypt' article from Auth0 provides a great explanation of bcrypt's history and internal workings.
Please read the following three sections: Motivation Behind bcrypt: This explains why an adaptive function was needed to keep pace with Moore's Law. How does 'bcrypt' work?: This gives a high-level overview of its internal two-phase process based on the slow Blowfish key schedule. bcrypt Best Practices: This section is critical. It discusses the 'work factor' and shows a table demonstrating the exponential relationship between the cost factor and the time it takes to compute a hash. Pay close attention to that table.
As you read, the key takeaway is that bcrypt's strength comes from exploiting the intentionally slow key setup phase of the Blowfish cipher. The work factor exponentially increases the number of times this slow process is run.
The final bcrypt hash string elegantly encodes all the necessary information to verify a password later:
$2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
$2b$: The bcrypt algorithm version.12: The work factor (cost). In this case, 212 iterations.R9h/cIPz0gi.URNNX3kh2O: The 128-bit salt, Base64 encoded.PST9/PgBkqquzi.Ss7KIUgO2t0jWMUW: The actual hashed output, Base64 encoded.
When you call a verification function like bcrypt.compare(password, hash), the library parses this string, extracts the salt and work factor, re-hashes the provided password with the exact same parameters, and performs a secure comparison.
Practical Implementation in Python
Let's see how this looks in Python, which is directly relevant to your work with Django. Django itself uses a pluggable password hashing system and recommends using bcrypt or Argon2.
The following code snippet is a best-practice implementation for hashing and verifying passwords with the bcrypt library in Python. It also includes a crucial detail for handling a known limitation of bcrypt.
Password Hashing Guide 2025: Argon2 vs Bcrypt vs Scrypt vs ...
This section from the 'Password Hashing Guide' provides production-ready Python code for bcrypt.
In the 'Python Implementation' section, find and study the code block containing the PasswordManager class. Specifically, focus on the two functions hash_bcrypt and verify_bcrypt. Notice how the code handles the 72-byte password limit by pre-hashing longer passwords with SHA-384. This is a subtle but important detail for robust implementations. You can read about this limit in the 'The 72-Byte Password Limit' section under the 'Bcrypt: The Proven Workhorse' heading.
The key functions from the bcrypt library are:
bcrypt.gensalt(rounds=12): Generates a random salt with a specified work factor.bcrypt.hashpw(password_bytes, salt): Hashes the password using the provided salt.bcrypt.checkpw(password_bytes, hash_bytes): The verification function. It securely compares a plaintext password against a stored hash and returnsTrueorFalse. This function handles the parsing of the hash string and performs a constant-time comparison to prevent timing attacks.
Common Mistakes to Avoid
Even when using a strong library, it's possible to make implementation mistakes that undermine security.
- Using an insufficient work factor: A work factor of 4 is useless. Follow OWASP recommendations (currently 10 minimum, 12+ recommended). Benchmark on your production hardware and aim for a hash time between 200-500ms.
- Not using a constant-time comparison: A simple string comparison like
hash1 == hash2can leak information through timing differences. Always use the library's built-in verification function (bcrypt.checkpwin Python). - Rolling your own crypto: Never try to invent your own hashing scheme. Use battle-tested libraries and algorithms.
- Using a static salt: The salt must be unique for every user. Good libraries handle this for you.
Conclusion
In this lesson, we've gone from the root of why we can't store plaintext passwords to the modern, robust solution provided by slow, adaptive hashing algorithms like bcrypt.
Key Takeaways:
- Hash, Don't Encrypt: Passwords must be processed with a one-way hash function.
- Salt Everything: A unique, random salt for every password defeats pre-computation attacks like rainbow tables.
- Slow is Strong: Use a deliberately slow algorithm like bcrypt with a configurable work factor. This makes brute-force attacks against a stolen database computationally infeasible.
- Be Adaptive: The work factor allows you to increase hashing difficulty over time as computing power increases, keeping your security robust.
- Use Libraries Correctly: Rely on trusted libraries like
bcryptand use their built-in functions for hashing (hashpw) and verification (checkpw) to avoid common pitfalls.
Preview of the Next Lesson
Now that we understand how to securely store a representation of a user's password, our next step is to explore how we use this to manage a user's logged-in state. In the next lesson, we will describe the flow of session-based authentication, including cookie management and server-side session storage. We'll see how a successful password verification kicks off a stateful relationship between the client and server.