Authentication and Authorization

Password Management

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.

Password Management

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.

Password Hashing and Storage

Django's Password Hashing System

# 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.