Introduction
Password security is a critical layer of authentication defense. Django includes a robust password hashing system with pluggable hashers and configurable validators, making it straightforward to enforce strong password policies out of the box.
Key Concepts
- Password Hasher: Algorithm that converts passwords into irreversible hashes for storage (Argon2, PBKDF2, BCrypt).
- Password Validators: Configurable rules that enforce complexity, length, and uniqueness requirements.
- Argon2: Winner of the Password Hashing Competition; memory-hard algorithm that resists GPU and ASIC attacks.
- Brute Force Protection: Rate limiting and lockout mechanisms that prevent automated password guessing.
Real World Context
Weak password hashing has led to massive credential breaches. When LinkedIn's SHA1-hashed passwords were leaked, millions were cracked within hours. Using a modern memory-hard hasher like Argon2 makes each cracking attempt orders of magnitude more expensive, buying time for users to change compromised passwords.
Deep Dive
Password Hashers
python# settings.py PASSWORD_HASHERS = [ 'django.contrib.auth.hashers.Argon2PasswordHasher', # Recommended 'django.contrib.auth.hashers.PBKDF2PasswordHasher', 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', ] # pip install argon2-cffi # For Argon2
Password Validators
python# settings.py AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 'OPTIONS': { 'user_attributes': ('username', 'email', 'first_name', 'last_name'), 'max_similarity': 0.7, } }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'OPTIONS': { 'min_length': 12, # Increase from default 8 } }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ]
Custom Password Validator
python# validators.py from django.core.exceptions import ValidationError import re class ComplexityValidator: """Require uppercase, lowercase, digit, and special character.""" def validate(self, password, user=None): if not re.search(r'[A-Z]', password): raise ValidationError( 'Password must contain at least one uppercase letter.', code='no_upper' ) if not re.search(r'[a-z]', password): raise ValidationError( 'Password must contain at least one lowercase letter.', code='no_lower' ) if not re.search(r'\d', password): raise ValidationError( 'Password must contain at least one digit.', code='no_digit' ) if not re.search(r'[!@#$%^&*(),.?\":{}|<>]', password): raise ValidationError( 'Password must contain at least one special character.', code='no_special' ) def get_help_text(self): return 'Password must contain uppercase, lowercase, digit, and special character.'
Brute Force Protection
python# pip install django-axes # settings.py INSTALLED_APPS = [ # ... 'axes', ] AUTHENTICATION_BACKENDS = [ 'axes.backends.AxesStandaloneBackend', 'django.contrib.auth.backends.ModelBackend', ] # Lock out after 5 failed attempts AXES_FAILURE_LIMIT = 5 # Lock out for 15 minutes AXES_COOLOFF_TIME = 0.25 # hours (15 minutes) # Lock by IP and username combination AXES_LOCKOUT_PARAMETERS = ['ip_address', 'username'] # Only lock out on POST to login URL AXES_ONLY_USER_FAILURES = False
Secure Password Reset
python# settings.py PASSWORD_RESET_TIMEOUT = 3600 # Token valid for 1 hour (default: 3 days) # Use HTTPS for password reset links if not DEBUG: USE_X_FORWARDED_HOST = True SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
Session Security
python# settings.py # Use database sessions for security SESSION_ENGINE = 'django.contrib.sessions.backends.db' # Secure cookie settings SESSION_COOKIE_SECURE = True # HTTPS only SESSION_COOKIE_HTTPONLY = True # No JavaScript access SESSION_COOKIE_SAMESITE = 'Lax' # CSRF protection SESSION_COOKIE_AGE = 1209600 # 2 weeks # Expire session on browser close SESSION_EXPIRE_AT_BROWSER_CLOSE = True # Rotate session on login def login_view(request): # ... authenticate user ... login(request, user) request.session.cycle_key() # New session ID
Two-Factor Authentication
python# pip install django-two-factor-auth # settings.py INSTALLED_APPS = [ # ... 'django_otp', 'django_otp.plugins.otp_static', 'django_otp.plugins.otp_totp', 'two_factor', ] LOGIN_URL = 'two_factor:login'
Common Pitfalls
- Using the default PBKDF2 when Argon2 is available — PBKDF2 is safe but Argon2 is significantly harder to crack with GPUs; install argon2-cffi and make it your first hasher.
- Setting PASSWORD_RESET_TIMEOUT too high — The default 3 days gives attackers a large window; reduce to 1 hour for production.
- Not validating passwords on the backend — Client-side validation is easily bypassed; always enforce validators server-side.
Best Practices
- Make Argon2 the primary hasher — Install argon2-cffi and list it first in PASSWORD_HASHERS.
- Increase minimum password length to 12+ — Length is more important than complexity rules for entropy.
- Combine hashers with rate limiting — Use django-axes alongside strong hashing for defense in depth.
Summary
- Configure Argon2 as your primary hasher and enforce a minimum 12-character password length.
- Use Django's built-in validators and add custom validators for domain-specific requirements.
- Pair strong hashing with brute force protection (django-axes) and two-factor authentication for complete password security.
Code Examples
python
# settings.py - Secure password configuration
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
]
AUTH_PASSWORD_VALIDATORS = [
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'OPTIONS': {'min_length': 12}},
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]
# pip install argon2-cffi