Proper password storage and cryptographic practices are fundamental to application security. Django provides robust built-in password hashing and cryptographic utilities, but understanding how to use them correctly is crucial for protecting user data.
# Django's password hashing system
from django.contrib.auth.hashers import make_password, check_password
from django.contrib.auth.models import User
# How Django stores passwords
user = User.objects.create_user(
username='testuser',
password='mypassword123'
)
# Password is automatically hashed and stored
print(user.password)
# Output: pbkdf2_sha256$260000$randomsalt$hashedpassword
# Password format: algorithm$iterations$salt$hash
# - algorithm: pbkdf2_sha256 (default)
# - iterations: 260000 (configurable)
# - salt: random salt for this password
# - hash: the actual password hash
# Verifying passwords
is_valid = user.check_password('mypassword123') # True
is_valid = user.check_password('wrongpassword') # False
# settings.py - Password hasher configuration
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.PBKDF2PasswordHasher', # Default
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', # Fallback
'django.contrib.auth.hashers.Argon2PasswordHasher', # Recommended
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', # Alternative
'django.contrib.auth.hashers.ScryptPasswordHasher', # Modern option
]
# Recommended configuration for new projects
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.Argon2PasswordHasher', # Primary
'django.contrib.auth.hashers.PBKDF2PasswordHasher', # Fallback
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', # Legacy support
]
# Argon2 configuration (requires argon2-cffi package)
# pip install argon2-cffi
# hashers.py - Custom password hasher
from django.contrib.auth.hashers import BasePasswordHasher
from django.utils.crypto import constant_time_compare
import hashlib
import secrets
class CustomPBKDF2Hasher(BasePasswordHasher):
"""Custom PBKDF2 hasher with enhanced security"""
algorithm = "custom_pbkdf2"
library = "hashlib"
def encode(self, password, salt):
"""Encode password with custom parameters"""
if not salt:
salt = secrets.token_hex(16) # 32-character hex salt
# Use higher iteration count for better security
iterations = 500000 # Increased from default 260000
hash = hashlib.pbkdf2_hmac(
'sha256',
password.encode('utf-8'),
salt.encode('utf-8'),
iterations
)
hash_hex = hash.hex()
return f"{self.algorithm}${iterations}${salt}${hash_hex}"
def verify(self, password, encoded):
"""Verify password against encoded hash"""
algorithm, iterations, salt, hash_hex = encoded.split('$', 3)
assert algorithm == self.algorithm
encoded_2 = self.encode(password, salt)
return constant_time_compare(encoded, encoded_2)
def safe_summary(self, encoded):
"""Return safe summary for admin display"""
algorithm, iterations, salt, hash_hex = encoded.split('$', 3)
return {
'algorithm': algorithm,
'iterations': iterations,
'salt': salt[:6] + '...', # Show only first 6 chars of salt
'hash': hash_hex[:6] + '...',
}
def harden_runtime(self, password, encoded):
"""Harden against timing attacks"""
pass
def must_update(self, encoded):
"""Check if password needs to be updated"""
algorithm, iterations, salt, hash_hex = encoded.split('$', 3)
# Update if iterations are too low
return int(iterations) < 400000
# Register custom hasher in settings
PASSWORD_HASHERS = [
'myapp.hashers.CustomPBKDF2Hasher',
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
]
# settings.py - Password validation configuration
AUTH_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, # Increased from default 8
}
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# validators.py - Custom password validators
from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _
import re
import requests
import hashlib
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 = []
# Count character types
uppercase_count = sum(1 for c in password if c.isupper())
lowercase_count = sum(1 for c in password if c.islower())
digit_count = sum(1 for c in password if c.isdigit())
special_count = sum(1 for c in password if not c.isalnum())
# Check requirements
if uppercase_count < self.min_uppercase:
errors.append(f"Password must contain at least {self.min_uppercase} uppercase letter(s)")
if lowercase_count < self.min_lowercase:
errors.append(f"Password must contain at least {self.min_lowercase} lowercase letter(s)")
if digit_count < self.min_digits:
errors.append(f"Password must contain at least {self.min_digits} digit(s)")
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"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 BreachedPasswordValidator:
"""Check password against known breached passwords"""
def __init__(self, check_online=False):
self.check_online = check_online
def validate(self, password, user=None):
"""Check if password has been breached"""
# Check against local common passwords first
if self.is_common_password(password):
raise ValidationError(
_("This password is too common and has been found in data breaches."),
code='password_breached'
)
# Optional: Check against HaveIBeenPwned API
if self.check_online and self.is_breached_online(password):
raise ValidationError(
_("This password has been found in data breaches and should not be used."),
code='password_breached_online'
)
def is_common_password(self, password):
"""Check against local list of common passwords"""
# This would check against a local database of common passwords
common_passwords = {
'password', '123456', 'password123', 'admin', 'qwerty',
'letmein', 'welcome', 'monkey', '1234567890'
}
return password.lower() in common_passwords
def is_breached_online(self, password):
"""Check password against HaveIBeenPwned API"""
try:
# 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:]
# 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 the response
for line in response.text.splitlines():
hash_suffix, count = line.split(':')
if hash_suffix == suffix:
return True # Password found in breach
return False
except Exception:
# If API is unavailable, don't block password
return False
def get_help_text(self):
return _("Password must not be found in known data breaches.")
class PatternValidator:
"""Validate against common password patterns"""
def validate(self, password, user=None):
"""Check for common password patterns"""
# Check for keyboard patterns
keyboard_patterns = [
'qwerty', 'asdf', 'zxcv', '1234', 'abcd'
]
password_lower = password.lower()
for pattern in keyboard_patterns:
if pattern in password_lower:
raise ValidationError(
_("Password contains a common keyboard pattern."),
code='keyboard_pattern'
)
# Check for repeated characters
if re.search(r'(.)\1{2,}', password): # 3+ repeated characters
raise ValidationError(
_("Password contains too many repeated characters."),
code='repeated_characters'
)
# Check for simple patterns
if re.match(r'^[a-zA-Z]+\d+$', password): # Letters followed by numbers
raise ValidationError(
_("Password follows a predictable pattern (letters followed by numbers)."),
code='predictable_pattern'
)
def get_help_text(self):
return _("Password must not contain common patterns or repeated characters.")
# Register custom validators
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',
},
{
'NAME': 'myapp.validators.ComplexityValidator',
'OPTIONS': {
'min_uppercase': 1,
'min_lowercase': 1,
'min_digits': 1,
'min_special': 1
}
},
{
'NAME': 'myapp.validators.BreachedPasswordValidator',
'OPTIONS': {'check_online': True}
},
{
'NAME': 'myapp.validators.PatternValidator',
},
]
# Django's built-in cryptographic utilities
from django.utils.crypto import get_random_string, constant_time_compare
from django.core.signing import Signer, TimestampSigner
import secrets
# Generate secure random strings
random_token = get_random_string(32) # 32-character random string
random_hex = secrets.token_hex(16) # 32-character hex string
random_url_safe = secrets.token_urlsafe(32) # URL-safe token
# Constant-time comparison (prevents timing attacks)
def verify_token(provided_token, stored_token):
"""Securely compare tokens"""
return constant_time_compare(provided_token, stored_token)
# Digital signatures
signer = Signer()
signed_value = signer.sign('my_value') # 'my_value:signature'
original_value = signer.unsign(signed_value) # 'my_value'
# Timestamp signatures (with expiration)
timestamp_signer = TimestampSigner()
signed_value = timestamp_signer.sign('my_value')
original_value = timestamp_signer.unsign(signed_value, max_age=3600) # 1 hour expiry
# encryption.py - Symmetric encryption utilities
from cryptography.fernet import Fernet
from django.conf import settings
import base64
import os
class FieldEncryption:
"""Utility class for field-level encryption"""
def __init__(self):
# Get encryption key from settings
key = getattr(settings, 'FIELD_ENCRYPTION_KEY', None)
if not key:
# Generate a new key (store this securely!)
key = Fernet.generate_key()
if isinstance(key, str):
key = key.encode()
self.cipher = Fernet(key)
def encrypt(self, plaintext):
"""Encrypt plaintext string"""
if not plaintext:
return plaintext
if isinstance(plaintext, str):
plaintext = plaintext.encode('utf-8')
encrypted = self.cipher.encrypt(plaintext)
return base64.urlsafe_b64encode(encrypted).decode('utf-8')
def decrypt(self, ciphertext):
"""Decrypt ciphertext string"""
if not ciphertext:
return ciphertext
try:
encrypted_data = base64.urlsafe_b64decode(ciphertext.encode('utf-8'))
decrypted = self.cipher.decrypt(encrypted_data)
return decrypted.decode('utf-8')
except Exception:
# Return original if decryption fails (for migration scenarios)
return ciphertext
# Encrypted model fields
from django.db import models
class EncryptedTextField(models.TextField):
"""Encrypted text field"""
def __init__(self, *args, **kwargs):
self.encryption = FieldEncryption()
super().__init__(*args, **kwargs)
def from_db_value(self, value, expression, connection):
"""Decrypt when loading from database"""
if value is None:
return value
return self.encryption.decrypt(value)
def to_python(self, value):
"""Convert to Python value"""
if isinstance(value, str) or value is None:
return value
return str(value)
def get_prep_value(self, value):
"""Encrypt when saving to database"""
if value is None:
return value
return self.encryption.encrypt(str(value))
# Usage in models
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
# Regular fields
bio = models.TextField(blank=True)
# Encrypted fields
ssn = EncryptedTextField(blank=True) # Social Security Number
phone = EncryptedTextField(blank=True) # Phone number
notes = EncryptedTextField(blank=True) # Private notes
# settings.py - Encryption key configuration
# Generate key: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
FIELD_ENCRYPTION_KEY = os.environ.get('FIELD_ENCRYPTION_KEY')
if not FIELD_ENCRYPTION_KEY:
raise ValueError("FIELD_ENCRYPTION_KEY must be set in environment variables")
# tokens.py - Secure token generation and management
import secrets
import hashlib
import time
from django.utils.crypto import constant_time_compare
from django.core.cache import cache
class SecureTokenManager:
"""Manage secure tokens for various purposes"""
def __init__(self, token_length=32):
self.token_length = token_length
def generate_token(self, purpose='general'):
"""Generate a secure random token"""
return secrets.token_urlsafe(self.token_length)
def generate_api_key(self, user_id, prefix='sk'):
"""Generate API key with user identification"""
# Format: prefix_userid_randomtoken
random_part = secrets.token_urlsafe(24)
return f"{prefix}_{user_id}_{random_part}"
def generate_reset_token(self, user):
"""Generate password reset token"""
# Include user info and timestamp for validation
timestamp = str(int(time.time()))
user_data = f"{user.id}:{user.email}:{timestamp}"
# Create token with user data hash
token = secrets.token_urlsafe(32)
token_hash = hashlib.sha256(f"{token}:{user_data}".encode()).hexdigest()
# Store token hash in cache with expiration
cache_key = f"reset_token:{user.id}"
cache.set(cache_key, {
'token_hash': token_hash,
'timestamp': timestamp
}, timeout=3600) # 1 hour expiry
return token
def verify_reset_token(self, user, token):
"""Verify password reset token"""
cache_key = f"reset_token:{user.id}"
stored_data = cache.get(cache_key)
if not stored_data:
return False
# Reconstruct user data
user_data = f"{user.id}:{user.email}:{stored_data['timestamp']}"
expected_hash = hashlib.sha256(f"{token}:{user_data}".encode()).hexdigest()
# Verify token hash
if constant_time_compare(stored_data['token_hash'], expected_hash):
# Check expiration (1 hour)
token_age = time.time() - int(stored_data['timestamp'])
if token_age <= 3600:
# Clean up used token
cache.delete(cache_key)
return True
return False
def generate_session_token(self, user, device_info=None):
"""Generate secure session token"""
token = secrets.token_urlsafe(48)
# Create token metadata
metadata = {
'user_id': user.id,
'created_at': time.time(),
'device_info': device_info or {},
'token_hash': hashlib.sha256(token.encode()).hexdigest()
}
# Store in cache with session timeout
cache_key = f"session_token:{token[:16]}" # Use prefix for lookup
cache.set(cache_key, metadata, timeout=86400) # 24 hours
return token
def verify_session_token(self, token):
"""Verify session token and return user info"""
cache_key = f"session_token:{token[:16]}"
metadata = cache.get(cache_key)
if not metadata:
return None
# Verify token hash
expected_hash = hashlib.sha256(token.encode()).hexdigest()
if not constant_time_compare(metadata['token_hash'], expected_hash):
return None
return metadata
# Usage in views
token_manager = SecureTokenManager()
def password_reset_request(request):
"""Handle password reset request"""
if request.method == 'POST':
email = request.POST.get('email')
try:
user = User.objects.get(email=email)
# Generate reset token
reset_token = token_manager.generate_reset_token(user)
# Send reset email
send_password_reset_email(user, reset_token)
messages.success(request, "Password reset email sent")
except User.DoesNotExist:
# Don't reveal if email exists
messages.success(request, "Password reset email sent")
return render(request, 'password_reset.html')
def password_reset_confirm(request, token):
"""Handle password reset confirmation"""
if request.method == 'POST':
email = request.POST.get('email')
new_password = request.POST.get('password')
try:
user = User.objects.get(email=email)
# Verify reset token
if token_manager.verify_reset_token(user, token):
# Update password
user.set_password(new_password)
user.save()
messages.success(request, "Password updated successfully")
return redirect('login')
else:
messages.error(request, "Invalid or expired reset token")
except User.DoesNotExist:
messages.error(request, "Invalid reset request")
return render(request, 'password_reset_confirm.html', {'token': token})
# Database-level encryption considerations
# settings.py - Database encryption configuration
# PostgreSQL with encryption
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'encrypted_db',
'USER': 'db_user',
'PASSWORD': os.environ.get('DB_PASSWORD'),
'HOST': 'localhost',
'PORT': '5432',
'OPTIONS': {
# Enable SSL
'sslmode': 'require',
'sslcert': '/path/to/client-cert.pem',
'sslkey': '/path/to/client-key.pem',
'sslrootcert': '/path/to/ca-cert.pem',
},
}
}
# Transparent Data Encryption (TDE) considerations:
# - Use database-level encryption for data at rest
# - Implement proper key management
# - Consider performance implications
# - Ensure backup encryption
# File encryption utilities
import os
from cryptography.fernet import Fernet
from django.core.files.storage import FileSystemStorage
class EncryptedFileSystemStorage(FileSystemStorage):
"""File system storage with encryption"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Get encryption key
key = os.environ.get('FILE_ENCRYPTION_KEY')
if not key:
key = Fernet.generate_key()
if isinstance(key, str):
key = key.encode()
self.cipher = Fernet(key)
def _save(self, name, content):
"""Encrypt file content before saving"""
# Read content
content.seek(0)
file_content = content.read()
# Encrypt content
encrypted_content = self.cipher.encrypt(file_content)
# Create new content file
from django.core.files.base import ContentFile
encrypted_file = ContentFile(encrypted_content)
# Save encrypted file
return super()._save(name, encrypted_file)
def _open(self, name, mode='rb'):
"""Decrypt file content when opening"""
# Open encrypted file
encrypted_file = super()._open(name, mode)
# Read and decrypt content
encrypted_content = encrypted_file.read()
decrypted_content = self.cipher.decrypt(encrypted_content)
# Return decrypted content
from django.core.files.base import ContentFile
return ContentFile(decrypted_content)
# Usage in models
class SecureDocument(models.Model):
title = models.CharField(max_length=200)
file = models.FileField(
upload_to='secure_docs/',
storage=EncryptedFileSystemStorage()
)
uploaded_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
# Key management best practices
import os
from django.core.exceptions import ImproperlyConfigured
def get_encryption_key(key_name):
"""Securely retrieve encryption key from environment"""
key = os.environ.get(key_name)
if not key:
raise ImproperlyConfigured(f"Encryption key {key_name} not found in environment")
# Validate key format
try:
# For Fernet keys, validate base64 format
import base64
decoded = base64.urlsafe_b64decode(key)
if len(decoded) != 32: # Fernet keys are 32 bytes
raise ValueError("Invalid key length")
except Exception:
raise ImproperlyConfigured(f"Invalid encryption key format for {key_name}")
return key
# Key rotation utilities
class KeyRotationManager:
"""Manage encryption key rotation"""
def __init__(self):
self.current_key = get_encryption_key('CURRENT_ENCRYPTION_KEY')
self.old_keys = self.load_old_keys()
def load_old_keys(self):
"""Load old encryption keys for decryption"""
old_keys = []
# Load old keys from environment
for i in range(1, 6): # Support up to 5 old keys
key_name = f'OLD_ENCRYPTION_KEY_{i}'
key = os.environ.get(key_name)
if key:
old_keys.append(key)
return old_keys
def encrypt(self, plaintext):
"""Encrypt with current key"""
cipher = Fernet(self.current_key.encode())
return cipher.encrypt(plaintext.encode()).decode()
def decrypt(self, ciphertext):
"""Decrypt with current or old keys"""
# Try current key first
try:
cipher = Fernet(self.current_key.encode())
return cipher.decrypt(ciphertext.encode()).decode()
except Exception:
pass
# Try old keys
for old_key in self.old_keys:
try:
cipher = Fernet(old_key.encode())
return cipher.decrypt(ciphertext.encode()).decode()
except Exception:
continue
raise ValueError("Unable to decrypt with any available key")
def needs_reencryption(self, ciphertext):
"""Check if data needs re-encryption with current key"""
try:
# If current key can decrypt, no re-encryption needed
cipher = Fernet(self.current_key.encode())
cipher.decrypt(ciphertext.encode())
return False
except Exception:
# If current key can't decrypt, re-encryption needed
return True
# Key rotation management command
from django.core.management.base import BaseCommand
class Command(BaseCommand):
"""Rotate encryption keys for encrypted fields"""
help = 'Rotate encryption keys for encrypted model fields'
def handle(self, *args, **options):
key_manager = KeyRotationManager()
# Find all models with encrypted fields
encrypted_models = [UserProfile] # Add your encrypted models
for model_class in encrypted_models:
self.rotate_model_keys(model_class, key_manager)
def rotate_model_keys(self, model_class, key_manager):
"""Rotate keys for a specific model"""
encrypted_fields = ['ssn', 'phone', 'notes'] # Your encrypted fields
for obj in model_class.objects.all():
updated = False
for field_name in encrypted_fields:
field_value = getattr(obj, field_name)
if field_value and key_manager.needs_reencryption(field_value):
# Decrypt with old key and re-encrypt with new key
try:
decrypted = key_manager.decrypt(field_value)
reencrypted = key_manager.encrypt(decrypted)
setattr(obj, field_name, reencrypted)
updated = True
except Exception as e:
self.stdout.write(
self.style.ERROR(f'Failed to rotate key for {model_class.__name__} {obj.id}: {e}')
)
if updated:
obj.save()
self.stdout.write(f'Rotated keys for {model_class.__name__} {obj.id}')
# tests.py - Cryptographic function tests
from django.test import TestCase
from django.contrib.auth.models import User
from django.contrib.auth.hashers import check_password
import secrets
class PasswordSecurityTests(TestCase):
"""Test password security functions"""
def test_password_hashing(self):
"""Test password hashing and verification"""
password = 'test_password_123!'
user = User.objects.create_user(
username='testuser',
password=password
)
# Password should be hashed
self.assertNotEqual(user.password, password)
self.assertTrue(user.password.startswith('pbkdf2_sha256$'))
# Password verification should work
self.assertTrue(user.check_password(password))
self.assertFalse(user.check_password('wrong_password'))
def test_password_validation(self):
"""Test custom password validators"""
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
# Test weak passwords
weak_passwords = [
'password', # Too common
'12345678', # All digits
'abcdefgh', # No complexity
'Password', # Missing digits/special chars
]
for weak_password in weak_passwords:
with self.assertRaises(ValidationError):
validate_password(weak_password)
# Test strong password
strong_password = 'MyStr0ng!P@ssw0rd2023'
try:
validate_password(strong_password)
except ValidationError:
self.fail("Strong password should pass validation")
def test_token_generation(self):
"""Test secure token generation"""
token_manager = SecureTokenManager()
# Test token uniqueness
tokens = [token_manager.generate_token() for _ in range(100)]
self.assertEqual(len(tokens), len(set(tokens))) # All unique
# Test token length
token = token_manager.generate_token()
self.assertGreaterEqual(len(token), 32)
# Test API key format
api_key = token_manager.generate_api_key(123)
self.assertTrue(api_key.startswith('sk_123_'))
def test_encryption_decryption(self):
"""Test field encryption and decryption"""
encryption = FieldEncryption()
# Test basic encryption/decryption
plaintext = "Sensitive information"
ciphertext = encryption.encrypt(plaintext)
self.assertNotEqual(plaintext, ciphertext)
self.assertEqual(plaintext, encryption.decrypt(ciphertext))
# Test empty values
self.assertEqual('', encryption.encrypt(''))
self.assertIsNone(encryption.encrypt(None))
def test_constant_time_comparison(self):
"""Test constant-time comparison function"""
from django.utils.crypto import constant_time_compare
# Test equal strings
self.assertTrue(constant_time_compare('hello', 'hello'))
# Test different strings
self.assertFalse(constant_time_compare('hello', 'world'))
# Test different lengths
self.assertFalse(constant_time_compare('hello', 'hello world'))
class CryptographicUtilityTests(TestCase):
"""Test cryptographic utility functions"""
def test_secure_random_generation(self):
"""Test secure random number generation"""
# Test randomness
random_values = [secrets.randbelow(1000000) for _ in range(1000)]
# Check for reasonable distribution (not perfect randomness test)
unique_values = len(set(random_values))
self.assertGreater(unique_values, 900) # Should be mostly unique
# Test token generation
tokens = [secrets.token_urlsafe(32) for _ in range(100)]
self.assertEqual(len(tokens), len(set(tokens))) # All unique
def test_key_rotation(self):
"""Test encryption key rotation"""
# This would test the key rotation functionality
# Implementation depends on your specific key rotation setup
pass
Now that you understand password storage and cryptography, let's explore the comprehensive secure deployment checklist to ensure your Django application is properly secured for production use.
HTTPS Setup and HSTS
HTTPS (HTTP Secure) is essential for protecting data in transit and ensuring the integrity and confidentiality of communications between clients and servers. This chapter covers implementing HTTPS in Django applications and configuring HTTP Strict Transport Security (HSTS) for enhanced security.
Secure Deployment Checklist
Deploying Django applications securely requires careful attention to configuration, infrastructure, and operational practices. This comprehensive checklist covers all aspects of production security to ensure your application is protected against common threats and vulnerabilities.