Lesson 22 / 26

Password Hashing

Never store plain passwords.

Salted, slow hashes

Django stores passwords as salted, deliberately slow hashes (PBKDF2 by default, with Argon2 and bcrypt available) and verifies them with check_password; the iteration count rises with each release to keep pace with hardware. Use the built-in auth system or make_password rather than inventing your own hashing. In FastAPI projects, use a maintained library (such as pwdlib or passlib with Argon2) for the same purpose.

Hashing and verifying a password, run

I ran this script with python manage.py shell --no-imports in the demo project (Django 6.1.1, SQLite). The default algorithm is PBKDF2-SHA256 with 1,500,000 iterations in Django 6.1 and a random salt, so hashing the same password twice gives different hashes; check_password verifies correctly.

from django.contrib.auth.hashers import check_password, make_password

h = make_password("correct horse battery staple")
algo, iterations, salt, digest = h.split("$")
print(algo, "iterations:", iterations, "| salted:", len(salt) > 0)
print("verify right:", check_password("correct horse battery staple", h))
print("verify wrong:", check_password("hunter2", h))
print("same password, different hash:", make_password("x") != make_password("x"))

Output:

pbkdf2_sha256 iterations: 1500000 | salted: True
verify right: True
verify wrong: False
same password, different hash: True

Consider Argon2

Adding argon2-cffi and listing the Argon2 hasher first upgrades password storage; old hashes are upgraded on next login.

Quick check: Why does the same password produce different hashes?

  • Hashes include the current time only
  • The hash function is broken
  • The password changes
  • Each hash uses a random salt
Answer

Each hash uses a random salt — Salts defeat precomputed attacks.