Switch Django to Argon2 Password Hashing
Upgrade Django password hashing to Argon2id: install argon2-cffi, put Argon2PasswordHasher first, let existing hashes upgrade on login, wrap old hashes to upgrade everyone now, tune memory and time cost, and size servers for login load.
Problem statement
Django's default password hasher, PBKDF2 with SHA-256 and a high iteration count, is solid. Argon2id, the winner of the Password Hashing Competition, is memory-hard, which makes cracking stolen hashes with GPUs and specialised hardware much more expensive. Django supports Argon2 natively once the argon2-cffi package is installed. Switching is low risk because Django upgrades hashes transparently at login, but you should understand the CPU and memory cost per login, and how to handle users who rarely log in.
pip install argon2-cffi(ordjango[argon2]).- Put
django.contrib.auth.hashers.Argon2PasswordHasherfirst inPASSWORD_HASHERS, keeping the others below it. - Existing PBKDF2 hashes keep working and are re-hashed with Argon2 on each user's next successful login.
- For users who never log in, optionally wrap old hashes with a custom hasher and a data migration.
- Tune
time_cost,memory_cost, andparallelismby subclassing only if the defaults are too slow or too fast on your servers. - Rate-limit login endpoints; slower hashing makes floods of login attempts more expensive for you too.
Step-by-step solution
1. Install and configure
PASSWORD_HASHERS = [
"django.contrib.auth.hashers.Argon2PasswordHasher",
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
"django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
"django.contrib.auth.hashers.ScryptPasswordHasher",
]
The first entry is used for new hashes. The rest verify existing hashes. Keep every hasher that might appear in your database in the list, or those users cannot log in.
2. Upgrades on login
When a user logs in with a password stored under a non-preferred hasher, Django verifies it, then re-hashes it with Argon2 and saves it. Check progress:
from django.contrib.auth import get_user_model
from collections import Counter
Counter(u.password.split("$", 1)[0] for u in get_user_model().objects.only("password").iterator())
3. Upgrade dormant accounts now (optional)
Inactive users keep old hashes indefinitely. Django's documentation describes a "wrapped" hasher for this case: a data migration hashes each stored PBKDF2 hash again with the new algorithm and saves it under a custom algorithm name, and the custom hasher's verify() repeats both steps (PBKDF2, then the new algorithm) when the user logs in. Once a wrapped user logs in, Django re-hashes their password directly with the preferred hasher.
Follow the "Password upgrading without requiring a login" section of the Django documentation exactly and test it against a copy of production data, because a mistake locks users out. For many sites, upgrading on login plus forcing a password reset for accounts dormant for years is simpler and just as effective.
4. Tune cost parameters
Django's Argon2 defaults follow current recommendations. To measure:
import timeit
from django.contrib.auth.hashers import make_password
print(timeit.timeit(lambda: make_password("correct horse battery staple"), number=10) / 10)
Aim for roughly 50-250 ms per hash on production hardware. Adjust by subclassing:
class TunedArgon2Hasher(Argon2PasswordHasher):
time_cost = 3
memory_cost = 65536 # KiB, 64 MiB
parallelism = 2
Memory cost multiplies by concurrent logins: 20 simultaneous logins at 64 MiB need over 1 GB. Size workers and memory accordingly.
5. Protect the login endpoint
Expensive hashing makes each login attempt cost you CPU and memory. Rate limits (edge, Nginx, or django-axes) prevent attackers from turning your hasher into a denial-of-service lever.
Verification check: create a user and confirm their password field starts with argon2$argon2id$; log in as an existing PBKDF2 user and confirm the hash changes to Argon2; a load test of the login endpoint stays within memory limits.
Explanation
Password hashes protect users if the database leaks. PBKDF2's cost is purely computational, which GPUs and ASICs parallelise well. Argon2id adds a configurable memory requirement per hash, so attackers need proportionally more memory per guess, which limits parallel cracking. Django stores the algorithm and parameters inside each hash string, so multiple algorithms coexist and hashes can be upgraded individually when users authenticate. That design makes the switch safe and gradual.
Edge cases / notes
- Tests should set
PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]in test settings for speed; never in production. - External systems that verify Django hashes (for example a legacy service) must support Argon2 before you switch.
- scrypt is also memory-hard and supported by Django; Argon2id is the more common choice.
- Django upgrades raise default work factors over time; upgrades on login pick those up automatically.
Next steps
- Defend the login form with Lock Out Brute-Force Logins With django-axes.
- Review baseline protections in Django Deployment Security Basics.
FAQ
How do I enable Argon2 in Django?
Install argon2-cffi and put Argon2PasswordHasher first in PASSWORD_HASHERS.
Will switching hashers log users out or break logins?
No, as long as the old hashers stay in the list. Hashes upgrade on each user's next login.
Is Argon2 better than PBKDF2?
Argon2id is memory-hard, which makes offline cracking more expensive. PBKDF2 remains acceptable with high iteration counts.
Does Argon2 slow down my site?
Only logins and password changes. Measure hash time and rate-limit login endpoints.