Never store passwords, and never hash them with SHA-256. Use a slow, salted password hash — bcrypt or Argon2 — designed to resist cracking. Learn why, and how to do it and verify correctly.
SHA-256 is designed to be fast — which is exactly wrong for passwords, because an attacker who steals your database can compute billions of guesses per second on a GPU. Password hashes are deliberately slow and memory-hard, so each guess costs real time and money, turning a full database dump into a slow, expensive crack instead of an instant one.
Attacker steals your users table. How fast can they crack it?
SHA-256 (fast): billions of guesses/sec on a GPU -> weak passwords
fall in seconds. Wrong tool.
bcrypt/Argon2 (slow, tuned): a few thousand guesses/sec -> the same
attack now takes years and serious hardware.
Password hashes are SLOW ON PURPOSE. You tune the cost so a login is
~fast for you but each attacker guess is painfully expensive.A salt is a unique random value stored alongside each hash. It ensures two users with the same password get different hashes, which defeats precomputed rainbow tables and stops an attacker from cracking many accounts at once. Modern password-hashing libraries generate and embed the salt for you — you should not manage it by hand.
Without salt: hash("password123") is the SAME for every user who
picks it -> one rainbow-table lookup cracks them all at once.
With salt: hash(salt_i + password) is UNIQUE per user ->
the attacker must crack each account separately, and rainbow
tables are useless.
salt random, unique per hash, stored WITH the hash (not secret)
pepper optional extra secret stored SEPARATELY (e.g. in a KMS),
so a DB dump alone isn't enough
Good libraries generate + embed the salt automatically. Don't DIY it.Use a maintained library that implements a modern algorithm — Argon2id (the current recommendation) or bcrypt. The library produces a single self-describing string containing the algorithm, cost parameters, salt, and hash, and its verify function re-derives and compares in constant time. You store that one string; you never store or see the raw password.
# pip install argon2-cffi
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher() # sensible Argon2id defaults, tunable
# on signup: store ONLY this string (algo+params+salt+hash, all in one)
stored = ph.hash("correct horse battery staple")
# $argon2id$v=19$m=65536,t=3,p=4$c29tZXNhbHQ$Rdesc...
# on login:
try:
ph.verify(stored, user_input) # constant-time, re-derives salt/params
if ph.check_needs_rehash(stored): # params upgraded? transparently rehash
stored = ph.hash(user_input)
except VerifyMismatchError:
reject_login()
# bcrypt (passlib/bcrypt) works the same way. Never write your own.Beyond the algorithm, a few practices decide whether this holds up. Tune the cost to your hardware, upgrade parameters over time (rehash on login), never log or email passwords, and enforce length over arcane composition rules. This overlaps with the Authentication course, which covers the surrounding login flow and sessions.
Checklist for password storage:
[ ] Argon2id (or bcrypt) — never SHA/MD5 for passwords
[ ] let the library handle salting
[ ] tune cost so a hash takes ~250-500ms on YOUR hardware
[ ] rehash on login when you raise the cost / change algo
[ ] check against breached-password lists (k-anonymity API); favor
length + passphrases over forced symbol rules
[ ] never log, email, or store the plaintext — you should never SEE it
[ ] pepper via a KMS for defense in depth (optional)
Login flow, sessions, MFA -> see the Authentication course.