Secure password management is critical for protecting user accounts and maintaining application security. Django provides robust password handling capabilities including hashing, validation, and secure storage. Understanding these features enables you to implement strong password policies and protect user credentials.
# Django's password hashing system
from django.contrib.auth.hashers import (
make_password, check_password, is_password_usable,
get_hasher, identify_hasher
)
from django.contrib.auth import get_user_model
from django.conf import settings
User = get_user_model()
class PasswordHashingSystem:
"""Understanding Django's password hashing system"""
@staticmethod
def password_hasher_configuration():
"""Configure password hashers in settings"""
# Django's default password hashers (in order of preference)
password_hashers = [
'django.contrib.auth.hashers.Argon2PasswordHasher', # Most secure (default)
'django.contrib.auth.hashers.PBKDF2PasswordHasher', # Fallback
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', # Legacy
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', # Alternative
'django.contrib.auth.hashers.ScryptPasswordHasher', # Alternative
]
# Custom hasher configuration
custom_settings = {
'PASSWORD_HASHERS': password_hashers,
# Argon2 configuration
'ARGON2_TIME_COST': 2, # Number of iterations
'ARGON2_MEMORY_COST': 512, # Memory usage in KB
'ARGON2_PARALLELISM': 2, # Number of parallel threads
# PBKDF2 configuration
'PBKDF2_ITERATIONS': 320000, # Number of iterations (Django 4.0+)
}
return custom_settings
@staticmethod
def password_hashing_examples():
"""Examples of password hashing operations"""
# Hash a password
plain_password = "secure_password123"
hashed_password = make_password(plain_password)
print(f"Plain password: {plain_password}")
print(f"Hashed password: {hashed_password}")
# Check password
is_correct = check_password(plain_password, hashed_password)
print(f"Password check: {is_correct}")
# Check if password is usable
is_usable = is_password_usable(hashed_password)
print(f"Password is usable: {is_usable}")
# Get hasher information
hasher = identify_hasher(hashed_password)
print(f"Hasher algorithm: {hasher.algorithm}")
return {
'hashed_password': hashed_password,
'is_correct': is_correct,
'is_usable': is_usable,
'algorithm': hasher.algorithm
}
@staticmethod
def custom_password_hasher():
"""Create a custom password hasher"""
from django.contrib.auth.hashers import BasePasswordHasher
import hashlib
import secrets
class CustomSHA256Hasher(BasePasswordHasher):
"""
Custom SHA256-based password hasher
Note: This is for demonstration - use Django's built-in hashers in production
"""
algorithm = "custom_sha256"
library = "hashlib"
def encode(self, password, salt):
"""Encode password with salt"""
if not salt:
salt = self.salt()
# Combine password and salt
combined = f"{salt}${password}"
# Hash multiple times for security
hash_value = combined.encode('utf-8')
for _ in range(10000): # 10,000 iterations
hash_value = hashlib.sha256(hash_value).digest()
# Convert to hex
hash_hex = hash_value.hex()
return f"{self.algorithm}${salt}${hash_hex}"
def verify(self, password, encoded):
"""Verify password against encoded hash"""
algorithm, salt, hash_value = encoded.split('$', 2)
# Encode the provided password
encoded_2 = self.encode(password, salt)
# Compare hashes
return encoded == encoded_2
def safe_summary(self, encoded):
"""Return safe summary of encoded password"""
algorithm, salt, hash_value = encoded.split('$', 2)
return {
'algorithm': algorithm,
'salt': salt[:6] + '...',
'hash': hash_value[:6] + '...',
}
def harden_runtime(self, password, encoded):
"""Harden against timing attacks"""
pass
def must_update(self, encoded):
"""Check if password needs to be updated"""
return False
return CustomSHA256Hasher
@staticmethod
def password_upgrade_mechanism():
"""Implement password upgrade mechanism"""
def upgrade_user_password(user, raw_password):
"""Upgrade user's password hash if needed"""
# Check if password needs upgrading
if user.password:
hasher = identify_hasher(user.password)
# Check if we're using the preferred hasher
preferred_hasher = get_hasher()
if hasher.algorithm != preferred_hasher.algorithm:
# Upgrade to preferred hasher
user.set_password(raw_password)
user.save(update_fields=['password'])
return True, f"Upgraded from {hasher.algorithm} to {preferred_hasher.algorithm}"
# Check if hasher parameters need updating
elif hasher.must_update(user.password):
user.set_password(raw_password)
user.save(update_fields=['password'])
return True, f"Updated {hasher.algorithm} parameters"
return False, "No upgrade needed"
def bulk_password_upgrade():
"""Upgrade passwords for all users (run during maintenance)"""
upgraded_count = 0
# This would typically be done when users log in
# Here's how you might do a bulk upgrade if you have access to plain passwords
for user in User.objects.all():
if user.password:
hasher = identify_hasher(user.password)
preferred_hasher = get_hasher()
if hasher.algorithm != preferred_hasher.algorithm:
# In practice, you can't upgrade without the plain password
# This would be done during login when you have access to it
print(f"User {user.username} needs password upgrade")
upgraded_count += 1
return upgraded_count
return upgrade_user_password, bulk_password_upgrade
# Password validation
class PasswordValidation:
"""Implement comprehensive password validation"""
@staticmethod
def django_password_validators():
"""Configure Django's built-in password validators"""
password_validators = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
'OPTIONS': {
'user_attributes': ('username', 'first_name', 'last_name', 'email'),
'max_similarity': 0.7,
}
},
{
'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',
},
]
return password_validators
@staticmethod
def custom_password_validators():
"""Create custom password validators"""
from django.contrib.auth.password_validation import CommonPasswordValidator
from django.core.exceptions import ValidationError
import re
class ComplexityValidator:
"""Validate password complexity requirements"""
def __init__(self, min_uppercase=1, min_lowercase=1, min_digits=1, min_special=1):
self.min_uppercase = min_uppercase
self.min_lowercase = min_lowercase
self.min_digits = min_digits
self.min_special = min_special
def validate(self, password, user=None):
"""Validate password complexity"""
errors = []
# Check uppercase letters
uppercase_count = len(re.findall(r'[A-Z]', password))
if uppercase_count < self.min_uppercase:
errors.append(
f"Password must contain at least {self.min_uppercase} uppercase letter(s)."
)
# Check lowercase letters
lowercase_count = len(re.findall(r'[a-z]', password))
if lowercase_count < self.min_lowercase:
errors.append(
f"Password must contain at least {self.min_lowercase} lowercase letter(s)."
)
# Check digits
digit_count = len(re.findall(r'\d', password))
if digit_count < self.min_digits:
errors.append(
f"Password must contain at least {self.min_digits} digit(s)."
)
# Check special characters
special_count = len(re.findall(r'[!@#$%^&*(),.?":{}|<>]', password))
if special_count < self.min_special:
errors.append(
f"Password must contain at least {self.min_special} special character(s)."
)
if errors:
raise ValidationError(errors)
def get_help_text(self):
"""Return help text for password requirements"""
return (
f"Your password must contain at least {self.min_uppercase} uppercase letter, "
f"{self.min_lowercase} lowercase letter, {self.min_digits} digit, "
f"and {self.min_special} special character."
)
class PasswordHistoryValidator:
"""Prevent reuse of recent passwords"""
def __init__(self, history_count=5):
self.history_count = history_count
def validate(self, password, user=None):
"""Check against password history"""
if user and hasattr(user, 'password_history'):
# Get recent password hashes
recent_passwords = user.password_history.order_by('-created_at')[:self.history_count]
for old_password in recent_passwords:
if check_password(password, old_password.password_hash):
raise ValidationError(
f"Password cannot be one of your last {self.history_count} passwords."
)
def get_help_text(self):
return f"Password cannot be one of your last {self.history_count} passwords."
class PasswordExpiryValidator:
"""Validate password age"""
def __init__(self, max_age_days=90):
self.max_age_days = max_age_days
def validate(self, password, user=None):
"""Check if password needs to be changed due to age"""
if user and user.password:
# Check when password was last changed
if hasattr(user, 'password_changed_at'):
password_age = timezone.now() - user.password_changed_at
if password_age.days > self.max_age_days:
raise ValidationError(
f"Password is {password_age.days} days old. "
f"Passwords must be changed every {self.max_age_days} days."
)
def get_help_text(self):
return f"Passwords must be changed every {self.max_age_days} days."
return ComplexityValidator, PasswordHistoryValidator, PasswordExpiryValidator
@staticmethod
def validate_password_strength():
"""Comprehensive password strength validation"""
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
def check_password_strength(password, user=None):
"""Check password strength and return detailed feedback"""
strength_score = 0
feedback = []
# Length check
if len(password) >= 12:
strength_score += 2
feedback.append("✓ Good length (12+ characters)")
elif len(password) >= 8:
strength_score += 1
feedback.append("⚠ Minimum length met (8+ characters)")
else:
feedback.append("✗ Too short (minimum 8 characters)")
# Character variety
has_upper = bool(re.search(r'[A-Z]', password))
has_lower = bool(re.search(r'[a-z]', password))
has_digit = bool(re.search(r'\d', password))
has_special = bool(re.search(r'[!@#$%^&*(),.?":{}|<>]', password))
variety_count = sum([has_upper, has_lower, has_digit, has_special])
if variety_count >= 4:
strength_score += 3
feedback.append("✓ Excellent character variety")
elif variety_count >= 3:
strength_score += 2
feedback.append("✓ Good character variety")
elif variety_count >= 2:
strength_score += 1
feedback.append("⚠ Basic character variety")
else:
feedback.append("✗ Poor character variety")
# Common patterns check
common_patterns = [
r'123', r'abc', r'qwerty', r'password', r'admin'
]
has_common_pattern = any(
re.search(pattern, password.lower()) for pattern in common_patterns
)
if not has_common_pattern:
strength_score += 1
feedback.append("✓ No common patterns detected")
else:
feedback.append("✗ Contains common patterns")
# Django validation
try:
validate_password(password, user)
strength_score += 1
feedback.append("✓ Passes Django validation")
except ValidationError as e:
feedback.extend([f"✗ {error}" for error in e.messages])
# Calculate strength level
if strength_score >= 6:
strength_level = "Very Strong"
elif strength_score >= 4:
strength_level = "Strong"
elif strength_score >= 2:
strength_level = "Moderate"
else:
strength_level = "Weak"
return {
'score': strength_score,
'max_score': 7,
'level': strength_level,
'feedback': feedback
}
return check_password_strength
# Password reset and recovery
class PasswordResetSystem:
"""Implement secure password reset functionality"""
@staticmethod
def secure_password_reset_tokens():
"""Generate secure password reset tokens"""
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.utils.crypto import constant_time_compare
import hashlib
class SecurePasswordResetTokenGenerator(PasswordResetTokenGenerator):
"""Enhanced password reset token generator"""
def _make_hash_value(self, user, timestamp):
"""Create hash value for token generation"""
# Include additional user data for security
login_timestamp = '' if user.last_login is None else user.last_login.replace(microsecond=0, tzinfo=None)
return (
str(user.pk) + user.password + str(login_timestamp) +
str(timestamp) + str(user.email)
)
def check_token(self, user, token):
"""Check if token is valid with additional security checks"""
# Check if user is active
if not user.is_active:
return False
# Check token age (24 hours max)
if not super().check_token(user, token):
return False
# Additional security: check if password was changed after token generation
# This would require storing token generation time
return True
# Custom token generator with shorter expiry
class ShortLivedTokenGenerator(PasswordResetTokenGenerator):
"""Token generator with shorter expiry time"""
def _num_seconds(self, dt):
"""Return number of seconds since epoch"""
return int((dt - datetime(2001, 1, 1)).total_seconds())
def _make_token_with_timestamp(self, user, timestamp, legacy=False):
"""Generate token with custom timestamp handling"""
# Reduce token lifetime to 1 hour (3600 seconds)
ts_b36 = base36.dumps(timestamp)
hash_string = salted_hmac(
self.key_salt,
self._make_hash_value(user, timestamp),
secret=self.secret,
algorithm=self.algorithm,
).hexdigest()[::2]
return f"{ts_b36}-{hash_string}"
return SecurePasswordResetTokenGenerator, ShortLivedTokenGenerator
@staticmethod
def password_reset_rate_limiting():
"""Implement rate limiting for password reset requests"""
from django.core.cache import cache
from django.http import HttpResponseTooManyRequests
def rate_limit_password_reset(email, max_attempts=3, window=3600):
"""Rate limit password reset attempts"""
cache_key = f"password_reset_{email}"
attempts = cache.get(cache_key, 0)
if attempts >= max_attempts:
return False, f"Too many password reset attempts. Try again in {window//60} minutes."
# Increment attempts
cache.set(cache_key, attempts + 1, window)
return True, f"Password reset email sent. {max_attempts - attempts - 1} attempts remaining."
def password_reset_view_with_rate_limiting(request):
"""Password reset view with rate limiting"""
if request.method == 'POST':
email = request.POST.get('email')
# Check rate limit
allowed, message = rate_limit_password_reset(email)
if not allowed:
return HttpResponseTooManyRequests(message)
# Proceed with password reset
try:
user = User.objects.get(email=email)
# Send password reset email
messages.success(request, "Password reset email sent.")
except User.DoesNotExist:
# Don't reveal if email exists
messages.success(request, "If the email exists, a reset link has been sent.")
return render(request, 'password_reset.html')
return rate_limit_password_reset, password_reset_view_with_rate_limiting
@staticmethod
def secure_password_reset_workflow():
"""Implement complete secure password reset workflow"""
from django.core.mail import send_mail
from django.urls import reverse
from django.contrib.sites.shortcuts import get_current_site
def initiate_password_reset(request, email):
"""Initiate password reset process"""
try:
user = User.objects.get(email=email, is_active=True)
except User.DoesNotExist:
# Don't reveal if user exists
return True, "If the email exists, a reset link has been sent."
# Generate token
token_generator = SecurePasswordResetTokenGenerator()
token = token_generator.make_token(user)
# Create reset URL
current_site = get_current_site(request)
reset_url = request.build_absolute_uri(
reverse('password_reset_confirm', kwargs={
'uidb64': urlsafe_base64_encode(force_bytes(user.pk)),
'token': token,
})
)
# Send email
subject = f"Password Reset - {current_site.name}"
message = f"""
Hello {user.get_full_name() or user.username},
You requested a password reset for your account at {current_site.name}.
Please click the link below to reset your password:
{reset_url}
This link will expire in 24 hours.
If you didn't request this reset, please ignore this email.
Best regards,
The {current_site.name} Team
"""
send_mail(
subject,
message,
settings.DEFAULT_FROM_EMAIL,
[user.email],
fail_silently=False,
)
# Log password reset request
import logging
logger = logging.getLogger(__name__)
logger.info(f"Password reset requested for user: {user.username}")
return True, "Password reset email sent."
def confirm_password_reset(uidb64, token, new_password):
"""Confirm password reset and set new password"""
try:
# Decode user ID
uid = force_str(urlsafe_base64_decode(uidb64))
user = User.objects.get(pk=uid)
except (TypeError, ValueError, OverflowError, User.DoesNotExist):
return False, "Invalid reset link."
# Check token
token_generator = SecurePasswordResetTokenGenerator()
if not token_generator.check_token(user, token):
return False, "Invalid or expired reset link."
# Validate new password
try:
validate_password(new_password, user)
except ValidationError as e:
return False, e.messages
# Set new password
user.set_password(new_password)
user.save()
# Log successful password reset
import logging
logger = logging.getLogger(__name__)
logger.info(f"Password reset completed for user: {user.username}")
return True, "Password reset successfully."
return initiate_password_reset, confirm_password_reset
# Password security monitoring
class PasswordSecurityMonitoring:
"""Monitor and analyze password security"""
@staticmethod
def password_breach_checking():
"""Check passwords against known breaches"""
import hashlib
import requests
def check_password_breach(password):
"""Check if password appears in known breaches using HaveIBeenPwned API"""
# Hash password with SHA-1
sha1_hash = hashlib.sha1(password.encode('utf-8')).hexdigest().upper()
# Use k-anonymity: send only first 5 characters
prefix = sha1_hash[:5]
suffix = sha1_hash[5:]
try:
# Query HaveIBeenPwned API
response = requests.get(
f"https://api.pwnedpasswords.com/range/{prefix}",
timeout=5
)
if response.status_code == 200:
# Check if our suffix appears in results
for line in response.text.splitlines():
hash_suffix, count = line.split(':')
if hash_suffix == suffix:
return True, int(count)
return False, 0
except requests.RequestException:
# If API is unavailable, don't block password change
return None, 0
return False, 0
def validate_password_not_breached(password):
"""Validator to check password against breaches"""
is_breached, count = check_password_breach(password)
if is_breached:
if count > 100:
raise ValidationError(
f"This password has been found in {count} data breaches. "
"Please choose a different password."
)
else:
# Just warn for passwords with low breach counts
pass
return check_password_breach, validate_password_not_breached
@staticmethod
def password_analytics():
"""Analyze password patterns and security"""
def analyze_user_passwords():
"""Analyze password patterns across all users"""
from collections import defaultdict
analytics = {
'total_users': User.objects.count(),
'users_with_passwords': User.objects.exclude(password='').count(),
'password_algorithms': defaultdict(int),
'weak_passwords': 0,
'expired_passwords': 0,
}
for user in User.objects.exclude(password=''):
# Analyze password hash algorithm
if user.password:
try:
hasher = identify_hasher(user.password)
analytics['password_algorithms'][hasher.algorithm] += 1
except ValueError:
analytics['password_algorithms']['unknown'] += 1
# Check for weak passwords (would need additional data)
# This is a simplified example
if len(user.password) < 100: # Rough estimate for weak hash
analytics['weak_passwords'] += 1
return analytics
def generate_password_security_report():
"""Generate comprehensive password security report"""
analytics = analyze_user_passwords()
report = {
'summary': analytics,
'recommendations': [],
'action_items': []
}
# Generate recommendations
if analytics['password_algorithms'].get('md5', 0) > 0:
report['recommendations'].append(
"Upgrade users from MD5 password hashing"
)
report['action_items'].append(
"Force password reset for users with MD5 hashes"
)
if analytics['weak_passwords'] > analytics['total_users'] * 0.1:
report['recommendations'].append(
"Implement stronger password requirements"
)
return report
return analyze_user_passwords, generate_password_security_report
Effective password management is essential for application security. By implementing strong hashing, comprehensive validation, secure reset mechanisms, and continuous monitoring, you can protect user accounts and maintain the integrity of your authentication system.
Permissions
Django's permission system provides fine-grained access control for your application's resources. Understanding how to create, assign, and check permissions enables you to build secure applications with precise authorization controls.
Authentication Views
Django provides built-in authentication views for common authentication workflows like login, logout, password change, and password reset. Understanding how to use and customize these views enables you to implement secure authentication flows that meet your application's specific requirements.