Hash Passwords Securely
Hash passwords and secure your app — Python web development.
Focus: hash passwords and secure your app
Every day, developers ship apps with passwords stored in plain text — and every day, attackers harvest those databases to break into thousands of accounts. If you've ever connected a login form to a database and quietly wondered whether storing the raw password is 'okay for now,' this lesson is for you. By the end, you'll know exactly how to hash passwords and secure your app with Python's industry-standard tools, so a data breach doesn't become a catastrophe.
The problem this lesson solves
Storing passwords as plain text is like leaving your house key under the mat — it's convenient, but anyone who finds it gets in. When a database leaks (and it will, eventually), every password you stored becomes public. Attackers exploit this in two ways:
- Credential stuffing — they try the leaked password on banking, email, and social media accounts, because most people reuse passwords.
- Password spraying — they use common passwords against many usernames to find weak accounts.
Even if your app isn't a target itself, your users' other accounts are. The cost of a breach — legal fines, lost trust, cleanup costs — is massive. This lesson gives you a baseline defense: hash passwords and secure your app so that even if your database leaks, the passwords remain useless.
Core concept / mental model
A hash function is a one-way mathematical transformation. You feed it a password, it produces a fixed-length string of characters that looks like gibberish. The key property: you can't reverse it to recover the original password. When a user logs in, you hash the input and compare the result to the stored hash — if they match, the password is correct.
Think of it like a blender: you put in fruit, you get a smoothie. You can't get the fruit back out, but you can blend another fruit and compare the taste to tell if it's the same.
But a plain hash (like MD5 or SHA-1) isn't enough. The same password always produces the same hash, so attackers can precompute tables of common passwords and their hashes (called rainbow tables) to reverse them instantly. That's where salting comes in: you add a random, unique value to each password before hashing. This makes every hash unique, even for identical passwords, and forces attackers to crack each one individually.
Finally, you need a slow hash. Fast hashes like SHA-256 let attackers try billions of guesses per second. Password-specific algorithms like bcrypt, scrypt, and Argon2 are deliberately slow and memory-hard, making brute-force attacks impractical.
How it works step by step
The process breaks down into a few clear steps:
- Generate a random salt for each user. This is typically 16 bytes of randomness.
- Concatenate the salt and password (or feed them separately) into the hash function.
- Compute the hash using a password-specific algorithm (bcrypt, scrypt, Argon2).
- Store the hash and salt together, often in a single string that includes the algorithm parameters.
- On login, re-hash the provided password with the stored salt and compare the result to the stored hash.
Here's a simple diagram in words:
- Registration:
password + salt → hash → store(salt, hash) - Login:
input_password + stored_salt → new_hash → compare(new_hash, stored_hash)
Hands-on walkthrough
Python's standard library has a built-in solution: hashlib with PBKDF2, but for new projects, you'll want a more secure algorithm. The best choices are:
argon2-cffi— current recommendation, winner of the Password Hashing Competition.bcrypt— widely used, battle-tested, with automatic salt handling.hashlib.pbkdf2_hmac— standard library, good for legacy or no-dependency situations.
Let's start with bcrypt, the most common choice:
import bcrypt
# Hash a password
password = "correct horse battery staple"
hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
print(hashed) # e.g., b'$2b$12$Z9v...'
# Verify a password
is_correct = bcrypt.checkpw(password.encode('utf-8'), hashed)
print(is_correct) # True
Notice that bcrypt automatically includes the salt in the hash string — you don't need to store it separately. The hash starts with $2b$ (algorithm version), then a cost factor like 12, then the salt and hash.
Now let's see a more complete registration and login flow, using a simple in-memory database:
import bcrypt
# Simulated user database
users = {}
def register(username, password):
hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
users[username] = hashed
def login(username, password):
stored_hash = users.get(username)
if not stored_hash:
return False
return bcrypt.checkpw(password.encode('utf-8'), stored_hash)
# Test the flow
register("alice", "hunter2")
print(login("alice", "hunter2")) # True
print(login("alice", "wrongpass")) # False
print(login("bob", "hunter2")) # False
For modern applications, Argon2 is my top recommendation. It's more resistant to GPU-based attacks and lets you tune memory usage:
from argon2 import PasswordHasher
ph = PasswordHasher()
hashed = ph.hash("correct horse battery staple")
print(hashed) # e.g., $argon2id$v=19$m=65536,t=3,p=4$...
# Verify
is_correct = ph.verify(hashed, "correct horse battery staple")
print(is_correct) # True
# Catch verification failure
try:
ph.verify(hashed, "wrong password")
except Exception as e:
print("Invalid password") # This is what you would catch
Compare options / when to choose what
Here's a comparison to help you decide:
| Option | Pros | Cons | Best for |
|---|---|---|---|
| bcrypt | Simple, automatic salt, battle-tested, built-in cost factor | Limited to 72 bytes per password, uses less memory | Most web apps today |
| Argon2 | Memory-hard, most secure, configurable, future-proof | Requires third-party library (argon2-cffi), newer |
New projects, high-security apps |
| PBKDF2 (hashlib) | Standard library, no dependencies | Not memory-hard, must manage salt yourself, faster attacks | Legacy systems, constrained environments |
| sha256 (plain) | Fast, simple | Not secure — attacker can break it instantly | Never — only for checksums |
When to choose what:
- For a new web app with a standard stack, use bcrypt or Argon2. Either is fine; Argon2 is more modern.
- If you can't install third-party libraries (e.g., a restricted environment), use hashlib.pbkdf2_hmac with a high iteration count and a random salt.
- Never use plain SHA-256 or MD5 for password storage — they're both too fast and unsalted hashes are vulnerable to rainbow tables.
Troubleshooting & edge cases
- 'password too long' error with bcrypt — bcrypt only uses the first 72 bytes. If your password exceeds that, pre-hash it with SHA-256 and pass the hex digest. But know that this reduces security slightly.
- Different hashes for the same password — this is normal because of the salt. Don't worry.
checkpwalways returns False — make sure you're passing the same encoded bytes. Double-check that the password is encoded to UTF-8.- Argon2 verifier fails with
InvalidHashError— your stored hash may be truncated or malformed. Re-hash the password. - Cost factor too low — if hashing is instant, set a higher cost. But too high will make login slow. Aim for ~100ms.
- User reuses the same password — with salt, each user's hash is unique. If two users share a password, their hashes differ — that's the point.
Pro tip: Use a password manager yourself — but as a developer, ensure your users' passwords are hashed with a modern algorithm, and never log them.
What you learned & what's next
You now understand why plain-text passwords are disastrous, how one-way hashing with salt protects your users, and the core differences between bcrypt, Argon2, and PBKDF2. You've also seen hands-on, runnable code to hash passwords and secure your app.
With this foundation, you're ready for the next lesson: implementing session-based authentication, where you'll integrate this password hashing into a full login flow with cookies and session tokens. That's where the security you've built becomes part of a complete, usable system.
Practice recap
Write a small script that registers three users with the same password and shows their hashes differ. Then try verifying with a wrong password and print a user-friendly 'Invalid credentials' message. Next, tweak the bcrypt cost factor from 12 to 4 and measure the hash time — notice the speed difference. This hands-on experiment solidifies why slow hashing matters.
Common mistakes
- Using a fast hash like SHA-256 and thinking it's enough — attackers can crack billions per second.
- Storing the salt separately and incorrectly concatenating — you must include the salt in the hash input and store it together.
- Forgetting to encode the password as bytes before hashing with bcrypt, leading to Unicode errors.
- Setting a bcrypt cost factor too low (e.g., 4) making hashing instant and vulnerable to brute force.
Variations
- Use Django's
make_passwordandcheck_passwordbuilt-in functions for a Django web app — they handle secure hashing for you. - Use
hashlib.scryptfor a memory-hard standard-library option that doesn't require third-party libraries. - Implement password hashing with
passliblibrary for a unified interface across multiple algorithms.
Real-world use cases
- Storing user passwords in a Django or Flask app's user database during sign-up.
- Migrating a legacy app from plain-text passwords to hashed versions on login, without forcing password reset.
- Building a cron job that regularly re-hashes existing passwords to a stronger algorithm as security standards evolve.
Key takeaways
- Never store passwords as plain text — always use a salted, slow hash.
- bcrypt and Argon2 are industry-standard; avoid fast hashes like SHA-256 for passwords.
- Salting ensures that equal passwords produce different hashes, protecting against rainbow tables and duplicate detection.
- Verification is done by re-hashing the input and comparing — the original password is never stored or recovered.
- Always use a unique salt per user and include it in the stored hash string.
- Choose a cost factor that makes hashing take ~100ms to slow down brute-force attacks without harming user experience.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.