State, Sessions, and Authentication
Handle user state securely and implement authentication and authorization flows.
Content
Password hashing and salting
Versions:
Watch & Learn
AI-discovered learning video
Sign in to watch the learning video for this topic.
Password Hashing and Salting — Securely Storing User Secrets (CS50 Web Programming)
"Passwords are like underwear: change them often, don’t share them, and don’t leave them lying around in a public database." — Probably me, after reading a DB dump.
You’ve already learned how to configure Flask sessions and where to store session data (server-side stores vs signed cookies). Now we’re stepping back a layer: how do you safely put users’ passwords into the database your ORM manages? That’s where password hashing and salting come in — the non-sexy, absolutely essential plumbing of authentication.
What this is (in one dramatic sentence)
Password hashing = turn a password into a fixed-length, irreversible fingerprint. Salting = add unique randomness so fingerprints aren’t identical across identical passwords.
Why this matters: if an attacker steals your users table, you don’t want plain text logins. You want slow, salted hashes that make cracking expensive and rainbow tables useless.
Quick reminder: how this ties to what you already know
- You store user records in a database (we covered Model/ORM and migrations). The password field in that model should never be plain text.
- When a user logs in you verify credentials and then create a session (server-side session store or a properly configured Flask session). But password verification happens first — compare the hashed password you stored with the hash of what the user submitted.
So this topic sits between your model layer and session creation: store hashed passwords in the DB, verify at login, then open a session.
Real-world analogies (because metaphors stick)
- Hashing = meat grinder. You put the steak (password) in, you get ground beef (hash). You cannot reassemble the steak.
- Salt = spice mix unique to each batch. If everyone uses the same spice, someone could identify identical burgers. Unique salts mean identical steaks produce different ground beef.
- Pepper = secret server-side additive. If an attacker steals the DB but not the pepper jar (an env var kept safe), it’s harder to reverse hashes.
Important properties of a good password hash
- One-way: cannot feasibly reverse.
- Deterministic given same input + salt: same password+salt gives same hash so you can verify.
- Slow / adjustable work factor: slows down brute-force. Increase cost as hardware improves.
- Unique per-password salt: prevents precomputed attack (rainbow tables).
- Well-tested libraries: don’t roll your own crypto.
Never use general-purpose fast hashes (MD5, SHA1, SHA256) for password storage. Use bcrypt, Argon2, PBKDF2, or scrypt.
Practical code (Flask + ORM style)
Example using Werkzeug (simple) and bcrypt (recommended for cost control). Werkzeug's generate_password_hash uses PBKDF2 by default; modern projects often use bcrypt or Argon2.
Registration (storing hashed password)
# Using werkzeug.security
from werkzeug.security import generate_password_hash
# in your user registration handler
password = request.form['password']
hash = generate_password_hash(password) # default: pbkdf2:sha256 with salt
# store `hash` in your users table (e.g., User.password_hash)
user = User(username=username, password_hash=hash)
db.session.add(user)
db.session.commit()
Login (verifying)
from werkzeug.security import check_password_hash
# in login handler
user = User.query.filter_by(username=username).first()
if user and check_password_hash(user.password_hash, submitted_password):
# password ok -> create session
session['user_id'] = user.id
else:
# invalid credentials
Note: modern libraries embed salt and cost parameters into the hash string, so you don’t need to manage salts separately.
Upgrading from legacy plaintext or weak hashes (migration strategy)
You’ve got users with bad hashes (or worse, plaintext). Best practice:
- Add a new column (e.g., password_hash_new) via migration, keep legacy data until migrated.
- On login: if user has legacy password or weak hash, verify using the old method, then immediately generate and store a new strong hash. This migrates users silently as they log in.
- For users who don’t log in, force password reset or bulk rehash with a secure flow (email token).
This avoids mass-reset headaches and minimizes exposure.
Salts, peppers, and storage details
- Salt: random per password. Stored with the hash (or embedded in it). Modern schemes (bcrypt, Argon2) include the salt in the hash string. You do not need to store a separate salt column anymore.
- Pepper: a global secret stored outside the DB (e.g., in an environment variable). You can prepend/append pepper to passwords before hashing. Pros: if DB is stolen but server secret isn’t, attacker still lacks full data. Cons: if pepper leaks you’re back to square one; and pepper complicates password resets and migration.
- Cost/work factor: store the parameters of the algorithm so you can increase cost later. Good libs include these parameters in the hash output.
Why not just hash with SHA256? (common misconception)
SHA256 is fast. That’s great for checksums, not passwords. Attackers can try millions of SHA256 guesses per second on GPUs. Use deliberately slow functions (bcrypt, Argon2) that are computationally expensive or memory-hard.
Security checklist (practical)
- Use a proven library: bcrypt, Argon2, or PBKDF2 from a reputable lib.
- Ensure per-password salt (usually automatic).
- Set an appropriate cost/work factor and plan to increase it over time.
- Never store plain text passwords.
- Use constant-time comparison functions (provided by libs) to avoid timing attacks.
- Do password resets via tokens — never email passwords.
- Consider a pepper if you have strict threat models and manage secrets safely.
Example: rehash on successful login (pseudo)
# After user successfully logs in
if needs_rehash(user.password_hash):
new_hash = generate_password_hash(submitted_password) # with new stronger params
user.password_hash = new_hash
db.session.commit()
This pattern lets you upgrade hashing algorithms gradually.
Closing: Key takeaways (the things you’ll use tomorrow)
- Never store plain text passwords. Ever.
- Use bcrypt / Argon2 / PBKDF2 via well-reviewed libraries — they handle salts and costs for you.
- Store the hash in your ORM-managed users table, verify at login, then create the session.
- Salt per password prevents rainbow table attacks; work factor slows brute force.
- Plan for migration: rehash users on login, and require reset where needed.
"This is the moment where the concept finally clicks." — Because now you know that hashing + salting = less nightmares the next time someone downloads your users table.
If you want, I can: provide a Flask blueprint with registration/login handlers using bcrypt, or show a migration pattern to convert old plaintext passwords safely. Which would help you sleep better at night?
Comments (0)
Please sign in to leave a comment.
No comments yet. Be the first to comment!