Web Development September 03, 2025 55 min read

Complete Django OTP Authentication Tutorial

Complete Django OTP Authentication Tutorial: Build Secure Two-Factor Authentication with Email Verification - 2025 Guide"

A
7AZZANI
18 views

Table of Contents

  • Loading table of contents...

How to Implement Secure Django OTP Authentication: Complete 2025 Guide with Email Verification

Master Django two-factor authentication with this comprehensive step-by-step tutorial covering OTP implementation, security best practices, and production deployment

Table of Contents

  1. Introduction to Django OTP Authentication
  2. Why Choose OTP Authentication for Django Applications?
  3. Prerequisites and Requirements
  4. Project Setup and Installation
  5. Database Models for OTP Management
  6. Email Configuration and SMTP Setup
  7. Authentication Views and Business Logic
  8. Frontend Implementation with TailwindCSS
  9. Advanced Security Features
  10. Testing Your Django OTP System
  11. Production Deployment Guide
  12. Performance Optimization
  13. Troubleshooting Common Issues
  14. Best Practices and Security Considerations
  15. Conclusion and Next Steps

Introduction to Django OTP Authentication

Django OTP (One-Time Password) authentication represents a critical security enhancement for modern web applications. This comprehensive guide demonstrates how to implement a professional-grade two-factor authentication system using Django, providing enhanced security through email-based OTP verification.

In today's cybersecurity landscape, traditional username-password authentication is insufficient. Data breaches and credential theft make two-factor authentication essential for protecting user accounts and sensitive data. This tutorial will walk you through creating a robust Django OTP system that rivals enterprise-grade solutions.

What You'll Learn

By following this guide, you'll master:

  • Complete OTP Authentication System: Build a full-featured two-factor authentication system from scratch
  • Professional UI Design: Create stunning login interfaces using TailwindCSS
  • Security Best Practices: Implement rate limiting, session management, and security logging
  • Production Deployment: Configure your system for real-world deployment with proper SMTP and security settings
  • Performance Optimization: Ensure your authentication system scales efficiently

Key Features of Our Implementation

Our Django OTP system includes:

  • ✅ Email-based OTP verification
  • ✅ Professional responsive UI with TailwindCSS
  • ✅ Rate limiting and brute-force protection
  • ✅ Comprehensive security logging
  • ✅ Session management and "Remember Me" functionality
  • ✅ Production-ready configuration
  • ✅ Comprehensive error handling
  • ✅ Mobile-responsive design
  • ✅ CSRF protection
  • ✅ IP address tracking

Why Choose OTP Authentication for Django Applications?

Security Benefits

Enhanced Account Protection: OTP authentication significantly reduces the risk of account compromise. Even if passwords are stolen, attackers cannot access accounts without the second factor.

Compliance Requirements: Many industries require two-factor authentication for regulatory compliance (HIPAA, PCI DSS, GDPR).

User Trust: Implementing 2FA demonstrates commitment to security, increasing user confidence in your platform.

Business Advantages

Reduced Support Costs: Fewer account compromises mean fewer support tickets and password reset requests.

Competitive Edge: Security features differentiate your application in the marketplace.

Scalability: Email-based OTP scales easily compared to SMS-based solutions.

Technical Benefits

Framework Integration: Django's robust authentication system makes OTP integration seamless.

Customization: Full control over the authentication flow and user experience.

Cost-Effective: Email-based OTP eliminates SMS costs while maintaining security.


Prerequisites and Requirements

Technical Prerequisites

Before starting this tutorial, ensure you have:

  • Python 3.8+: Latest Python version for optimal performance
  • Django 4.0+: Modern Django framework with latest security features
  • Basic Django Knowledge: Understanding of models, views, templates, and URL routing
  • HTML/CSS Familiarity: Basic frontend development skills
  • Database Setup: PostgreSQL, MySQL, or SQLite for development

Development Environment

Set up your development environment with these tools:

  • Code Editor: VS Code, PyCharm, or your preferred IDE
  • Git: Version control for project management
  • Virtual Environment: Python venv or pipenv for dependency isolation
  • Email Service: Gmail, SendGrid, or similar SMTP provider

Required Python Packages

Our implementation uses these carefully selected packages:

# Core Django packages
Django>=4.0.0
python-dotenv>=0.19.0

# Form handling and UI
django-crispy-forms>=1.14.0
crispy-tailwind>=0.5.0

# Email and validation
django-email-validator>=2.0.0

# Security and rate limiting
django-ratelimit>=3.0.0

# Production deployment
gunicorn>=20.0.0
whitenoise>=6.0.0

# Database drivers (choose one)
psycopg2-binary>=2.9.0  # PostgreSQL
mysqlclient>=2.1.0      # MySQL

Project Setup and Installation

Creating Your Django Project

Start by setting up a clean Django project structure:

# Create project directory
mkdir django-otp-auth
cd django-otp-auth

# Create virtual environment
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

# Install Django
pip install django

# Create Django project
django-admin startproject otpauth .
cd otpauth

# Create accounts app
python manage.py startapp accounts

# Install additional packages
pip install python-dotenv django-crispy-forms crispy-tailwind

Initial Django Configuration

Configure your settings.py for optimal development:

# otpauth/settings.py
import os
from dotenv import load_dotenv
from pathlib import Path

load_dotenv()

BASE_DIR = Path(__file__).resolve().parent.parent

# Security settings
SECRET_KEY = os.getenv('SECRET_KEY', 'your-secret-key-here')
DEBUG = os.getenv('DEBUG', 'True').lower() == 'true'
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',')

# Application definition
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    
    # Third-party apps
    'crispy_forms',
    'crispy_tailwind',
    
    # Local apps
    'accounts',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'otpauth.urls'

# Template configuration
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

# Database configuration
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

# Static files configuration
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [BASE_DIR / 'static']

# Media files
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'

# Crispy Forms configuration
CRISPY_ALLOWED_TEMPLATE_PACKS = "tailwind"
CRISPY_TEMPLATE_PACK = "tailwind"

# Session configuration
SESSION_COOKIE_AGE = 3600  # 1 hour default
SESSION_SAVE_EVERY_REQUEST = True
SESSION_EXPIRE_AT_BROWSER_CLOSE = True

# Security settings
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'

# Logging configuration
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'file': {
            'level': 'INFO',
            'class': 'logging.FileHandler',
            'filename': 'django.log',
        },
        'security_file': {
            'level': 'WARNING',
            'class': 'logging.FileHandler',
            'filename': 'security.log',
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'INFO',
            'propagate': True,
        },
        'security': {
            'handlers': ['security_file'],
            'level': 'WARNING',
            'propagate': True,
        },
    },
}

Environment Variables Setup

Create a .env file in your project root:

# .env
SECRET_KEY=your-super-secret-django-key-here
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1

# Email configuration (Development)
EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend

# Email configuration (Production)
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_HOST_USER=your-email@gmail.com
EMAIL_HOST_PASSWORD=your-app-password
DEFAULT_FROM_EMAIL=Your App <noreply@yourapp.com>

# Database (if using PostgreSQL in production)
DATABASE_URL=postgresql://user:password@localhost:5432/dbname

Database Models for OTP Management

Core Model Architecture

Our OTP system requires three main models: UserProfile, EmailOTP, and LoginAttempt. These models provide comprehensive user management, OTP handling, and security logging.

# accounts/models.py
from django.contrib.auth.models import User
from django.db import models
from django.utils import timezone
from django.core.exceptions import ValidationError
import secrets
import string
import hashlib

class UserProfile(models.Model):
    """Extended user profile with OTP preferences"""
    user = models.OneToOneField(
        User, 
        on_delete=models.CASCADE, 
        related_name='profile'
    )
    otp_enabled = models.BooleanField(
        default=False,
        help_text="Enable two-factor authentication for this user"
    )
    phone_number = models.CharField(
        max_length=15, 
        blank=True, 
        help_text="Phone number for SMS backup (future feature)"
    )
    backup_codes = models.JSONField(
        default=list,
        help_text="Emergency backup codes for account recovery"
    )
    last_otp_request = models.DateTimeField(
        null=True, 
        blank=True,
        help_text="Timestamp of last OTP request for rate limiting"
    )
    failed_otp_attempts = models.IntegerField(
        default=0,
        help_text="Counter for failed OTP attempts"
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    class Meta:
        verbose_name = "User Profile"
        verbose_name_plural = "User Profiles"
        indexes = [
            models.Index(fields=['user']),
            models.Index(fields=['otp_enabled']),
        ]
    
    def __str__(self):
        return f"{self.user.username} Profile"
    
    def can_request_otp(self):
        """Check if user can request a new OTP (rate limiting)"""
        if not self.last_otp_request:
            return True
        
        time_diff = timezone.now() - self.last_otp_request
        return time_diff.total_seconds() > 60  # 1 minute cooldown
    
    def generate_backup_codes(self, count=8):
        """Generate emergency backup codes"""
        codes = []
        for _ in range(count):
            code = ''.join(secrets.choice(string.ascii_uppercase + string.digits) 
                          for _ in range(8))
            codes.append(code)
        
        # Hash codes before storing
        self.backup_codes = [hashlib.sha256(code.encode()).hexdigest() 
                           for code in codes]
        self.save()
        
        return codes  # Return unhashed codes for user to save
    
    def verify_backup_code(self, code):
        """Verify a backup code"""
        code_hash = hashlib.sha256(code.upper().encode()).hexdigest()
        if code_hash in self.backup_codes:
            # Remove used backup code
            self.backup_codes.remove(code_hash)
            self.save()
            return True
        return False

class EmailOTP(models.Model):
    """Email-based OTP management"""
    PURPOSE_CHOICES = [
        ('login', 'Login'),
        ('register', 'Registration'),
        ('password_reset', 'Password Reset'),
        ('email_change', 'Email Change'),
        ('enable_2fa', 'Enable 2FA'),
        ('disable_2fa', 'Disable 2FA'),
    ]
    
    STATUS_CHOICES = [
        ('active', 'Active'),
        ('used', 'Used'),
        ('expired', 'Expired'),
        ('revoked', 'Revoked'),
    ]
    
    user = models.ForeignKey(
        User, 
        on_delete=models.CASCADE,
        related_name='otp_codes'
    )
    otp_code = models.CharField(
        max_length=6,
        help_text="6-digit OTP code"
    )
    purpose = models.CharField(
        max_length=20, 
        choices=PURPOSE_CHOICES,
        help_text="Purpose of this OTP"
    )
    status = models.CharField(
        max_length=10,
        choices=STATUS_CHOICES,
        default='active'
    )
    created_at = models.DateTimeField(auto_now_add=True)
    expires_at = models.DateTimeField(
        help_text="OTP expiration time"
    )
    used_at = models.DateTimeField(
        null=True, 
        blank=True,
        help_text="Timestamp when OTP was used"
    )
    ip_address = models.GenericIPAddressField(
        null=True, 
        blank=True,
        help_text="IP address from which OTP was requested"
    )
    user_agent = models.TextField(
        blank=True,
        help_text="Browser user agent string"
    )
    attempts = models.IntegerField(
        default=0,
        help_text="Number of verification attempts"
    )
    max_attempts = models.IntegerField(
        default=3,
        help_text="Maximum allowed attempts"
    )
    
    class Meta:
        verbose_name = "Email OTP"
        verbose_name_plural = "Email OTPs"
        indexes = [
            models.Index(fields=['user', 'purpose', 'status']),
            models.Index(fields=['expires_at']),
            models.Index(fields=['created_at']),
        ]
        ordering = ['-created_at']
    
    def __str__(self):
        return f"OTP for {self.user.username} - {self.purpose}"
    
    @classmethod
    def generate_otp(cls, user, purpose, ip_address=None, user_agent='', expiry_minutes=10):
        """Generate a new 6-digit OTP"""
        # Revoke existing active OTPs for same purpose
        cls.objects.filter(
            user=user,
            purpose=purpose,
            status='active'
        ).update(status='revoked')
        
        # Generate secure OTP
        otp_code = ''.join(secrets.choice(string.digits) for _ in range(6))
        expires_at = timezone.now() + timezone.timedelta(minutes=expiry_minutes)
        
        otp = cls.objects.create(
            user=user,
            otp_code=otp_code,
            purpose=purpose,
            expires_at=expires_at,
            ip_address=ip_address,
            user_agent=user_agent
        )
        
        # Update user profile
        if hasattr(user, 'profile'):
            user.profile.last_otp_request = timezone.now()
            user.profile.save()
        
        return otp
    
    def is_valid(self):
        """Check if OTP is still valid"""
        if self.status != 'active':
            return False
        
        if timezone.now() > self.expires_at:
            self.status = 'expired'
            self.save()
            return False
        
        if self.attempts >= self.max_attempts:
            self.status = 'expired'
            self.save()
            return False
        
        return True
    
    def verify(self, code):
        """Verify OTP code"""
        self.attempts += 1
        self.save()
        
        if not self.is_valid():
            return False
        
        if self.otp_code == code:
            self.status = 'used'
            self.used_at = timezone.now()
            self.save()
            
            # Reset failed attempts on user profile
            if hasattr(self.user, 'profile'):
                self.user.profile.failed_otp_attempts = 0
                self.user.profile.save()
            
            return True
        
        # Increment failed attempts
        if hasattr(self.user, 'profile'):
            self.user.profile.failed_otp_attempts += 1
            self.user.profile.save()
        
        return False
    
    def clean(self):
        """Model validation"""
        if self.expires_at and self.expires_at <= timezone.now():
            raise ValidationError("Expiration time must be in the future")

class LoginAttempt(models.Model):
    """Security logging for login attempts"""
    RESULT_CHOICES = [
        ('success', 'Success'),
        ('invalid_credentials', 'Invalid Credentials'),
        ('invalid_otp', 'Invalid OTP'),
        ('expired_otp', 'Expired OTP'),
        ('rate_limited', 'Rate Limited'),
        ('account_locked', 'Account Locked'),
    ]
    
    user = models.ForeignKey(
        User, 
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name='login_attempts'
    )
    username = models.CharField(
        max_length=150,
        help_text="Username attempted (even if user doesn't exist)"
    )
    ip_address = models.GenericIPAddressField()
    user_agent = models.TextField(blank=True)
    result = models.CharField(
        max_length=20,
        choices=RESULT_CHOICES
    )
    details = models.JSONField(
        default=dict,
        help_text="Additional details about the attempt"
    )
    timestamp = models.DateTimeField(auto_now_add=True)
    session_key = models.CharField(max_length=40, blank=True)
    
    class Meta:
        verbose_name = "Login Attempt"
        verbose_name_plural = "Login Attempts"
        indexes = [
            models.Index(fields=['ip_address', 'timestamp']),
            models.Index(fields=['user', 'timestamp']),
            models.Index(fields=['result']),
        ]
        ordering = ['-timestamp']
    
    def __str__(self):
        return f"Login attempt by {self.username} - {self.result}"
    
    @classmethod
    def log_attempt(cls, username, ip_address, result, user=None, user_agent='', 
                   session_key='', **details):
        """Log a login attempt"""
        return cls.objects.create(
            user=user,
            username=username,
            ip_address=ip_address,
            user_agent=user_agent,
            result=result,
            details=details,
            session_key=session_key
        )
    
    @classmethod
    def get_recent_failures(cls, ip_address, minutes=15):
        """Get recent failed attempts from IP"""
        since = timezone.now() - timezone.timedelta(minutes=minutes)
        return cls.objects.filter(
            ip_address=ip_address,
            timestamp__gte=since,
            result__in=['invalid_credentials', 'invalid_otp', 'expired_otp']
        ).count()

class SecurityEvent(models.Model):
    """Additional security event logging"""
    EVENT_TYPES = [
        ('otp_enabled', 'OTP Enabled'),
        ('otp_disabled', 'OTP Disabled'),
        ('backup_codes_generated', 'Backup Codes Generated'),
        ('backup_code_used', 'Backup Code Used'),
        ('suspicious_activity', 'Suspicious Activity'),
        ('rate_limit_exceeded', 'Rate Limit Exceeded'),
    ]
    
    user = models.ForeignKey(
        User, 
        on_delete=models.CASCADE,
        related_name='security_events'
    )
    event_type = models.CharField(max_length=30, choices=EVENT_TYPES)
    description = models.TextField()
    ip_address = models.GenericIPAddressField()
    user_agent = models.TextField(blank=True)
    metadata = models.JSONField(default=dict)
    timestamp = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        verbose_name = "Security Event"
        verbose_name_plural = "Security Events"
        ordering = ['-timestamp']
    
    def __str__(self):
        return f"{self.event_type} - {self.user.username}"

Database Migrations

Create and apply the migrations:

# Create initial migration
python manage.py makemigrations accounts

# Apply migrations
python manage.py migrate

# Create superuser
python manage.py createsuperuser

Model Admin Configuration

Configure Django admin for easy management:

# accounts/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import User
from .models import UserProfile, EmailOTP, LoginAttempt, SecurityEvent

class UserProfileInline(admin.StackedInline):
    model = UserProfile
    can_delete = False
    verbose_name_plural = 'Profile'

class UserAdmin(BaseUserAdmin):
    inlines = (UserProfileInline,)
    list_display = ('username', 'email', 'first_name', 'last_name', 
                   'is_staff', 'get_otp_enabled')
    list_filter = ('is_staff', 'is_superuser', 'is_active', 'date_joined')
    
    def get_otp_enabled(self, obj):
        try:
            return obj.profile.otp_enabled
        except UserProfile.DoesNotExist:
            return False
    get_otp_enabled.boolean = True
    get_otp_enabled.short_description = 'OTP Enabled'

# Re-register UserAdmin
admin.site.unregister(User)
admin.site.register(User, UserAdmin)

@admin.register(EmailOTP)
class EmailOTPAdmin(admin.ModelAdmin):
    list_display = ('user', 'purpose', 'status', 'created_at', 'expires_at', 
                   'attempts', 'ip_address')
    list_filter = ('purpose', 'status', 'created_at')
    search_fields = ('user__username', 'user__email', 'ip_address')
    readonly_fields = ('otp_code', 'created_at', 'used_at')
    
    fieldsets = (
        (None, {
            'fields': ('user', 'purpose', 'status')
        }),
        ('OTP Details', {
            'fields': ('otp_code', 'attempts', 'max_attempts')
        }),
        ('Timestamps', {
            'fields': ('created_at', 'expires_at', 'used_at')
        }),
        ('Security', {
            'fields': ('ip_address', 'user_agent')
        }),
    )

@admin.register(LoginAttempt)
class LoginAttemptAdmin(admin.ModelAdmin):
    list_display = ('username', 'result', 'ip_address', 'timestamp')
    list_filter = ('result', 'timestamp')
    search_fields = ('username', 'ip_address')
    readonly_fields = ('timestamp',)
    
    def has_add_permission(self, request):
        return False
    
    def has_change_permission(self, request, obj=None):
        return False

@admin.register(SecurityEvent)
class SecurityEventAdmin(admin.ModelAdmin):
    list_display = ('user', 'event_type', 'ip_address', 'timestamp')
    list_filter = ('event_type', 'timestamp')
    search_fields = ('user__username', 'description', 'ip_address')
    readonly_fields = ('timestamp',)

Email Configuration and SMTP Setup

Development Email Backend

For development, Django provides a console backend that prints emails to the terminal:

# settings.py (Development)
if DEBUG:
    EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
else:
    # Production settings (see below)
    pass

Production SMTP Configuration

Configure production email settings for various providers:

# settings.py (Production Email Settings)

# Gmail Configuration
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD')  # App password
EMAIL_TIMEOUT = 15

# SendGrid Configuration
# EMAIL_HOST = 'smtp.sendgrid.net'
# EMAIL_PORT = 587
# EMAIL_HOST_USER = 'apikey'
# EMAIL_HOST_PASSWORD = os.getenv('SENDGRID_API_KEY')

# Mailgun Configuration
# EMAIL_HOST = 'smtp.mailgun.org'
# EMAIL_PORT = 587
# EMAIL_HOST_USER = os.getenv('MAILGUN_SMTP_LOGIN')
# EMAIL_HOST_PASSWORD = os.getenv('MAILGUN_SMTP_PASSWORD')

# Common settings
DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'Your App <noreply@yourapp.com>')
SERVER_EMAIL = DEFAULT_FROM_EMAIL

# Email security settings
EMAIL_USE_SSL = False  # Use TLS instead
EMAIL_SSL_CERTFILE = None
EMAIL_SSL_KEYFILE = None

Email Utilities and Templates

Create comprehensive email utilities:

# accounts/utils.py
from django.core.mail import send_mail, EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
from django.utils import timezone
import logging

logger = logging.getLogger(__name__)

def get_client_ip(request):
    """Extract client IP address from request"""
    x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
    if x_forwarded_for:
        ip = x_forwarded_for.split(',')[0].strip()
    else:
        ip = request.META.get('REMOTE_ADDR', '127.0.0.1')
    return ip

def get_user_agent(request):
    """Extract user agent from request"""
    return request.META.get('HTTP_USER_AGENT', '')

def get_device_info(user_agent):
    """Parse basic device information from user agent"""
    if not user_agent:
        return "Unknown Device"
    
    user_agent = user_agent.lower()
    
    # Mobile detection
    if any(mobile in user_agent for mobile in ['mobile', 'android', 'iphone']):
        if 'android' in user_agent:
            return "Android Device"
        elif 'iphone' in user_agent:
            return "iPhone"
        else:
            return "Mobile Device"
    
    # Desktop browsers
    if 'chrome' in user_agent:
        return "Chrome Browser"
    elif 'firefox' in user_agent:
        return "Firefox Browser"
    elif 'safari' in user_agent:
        return "Safari Browser"
    elif 'edge' in user_agent:
        return "Edge Browser"
    
    return "Desktop Browser"

def send_otp_email(user, otp, request=None):
    """Send professional OTP email with HTML template"""
    try:
        # Get request details
        ip_address = get_client_ip(request) if request else 'Unknown'
        device_info = get_device_info(get_user_agent(request)) if request else 'Unknown Device'
        
        # Email context
        context = {
            'user': user,
            'otp_code': otp.otp_code,
            'purpose': otp.get_purpose_display(),
            'expires_at': otp.expires_at,
            'ip_address': ip_address,
            'device_info': device_info,
            'timestamp': timezone.now(),
            'app_name': getattr(settings, 'APP_NAME', 'Your App'),
            'support_email': getattr(settings, 'SUPPORT_EMAIL', 'support@yourapp.com'),
        }
        
        # Render email templates
        subject = f"Your {otp.get_purpose_display()} Verification Code"
        html_message = render_to_string('emails/otp_email.html', context)
        plain_message = render_to_string('emails/otp_email.txt', context)
        
        # Create email message
        email = EmailMultiAlternatives(
            subject=subject,
            body=plain_message,
            from_email=settings.DEFAULT_FROM_EMAIL,
            to=[user.email],
        )
        email.attach_alternative(html_message, "text/html")
        
        # Send email
        email.send(fail_silently=False)
        
        logger.info(f"OTP email sent successfully to {user.email} for {otp.purpose}")
        return True
        
    except Exception as e:
        logger.error(f"Failed to send OTP email to {user.email}: {str(e)}")
        return False

def send_security_alert_email(user, event_type, details, request=None):
    """Send security alert email"""
    try:
        ip_address = get_client_ip(request) if request else 'Unknown'
        device_info = get_device_info(get_user_agent(request)) if request else 'Unknown Device'
        
        context = {
            'user': user,
            'event_type': event_type,
            'details': details,
            'ip_address': ip_address,
            'device_info': device_info,
            'timestamp': timezone.now(),
            'app_name': getattr(settings, 'APP_NAME', 'Your App'),
        }
        
        subject = f"Security Alert for Your Account"
        html_message = render_to_string('emails/security_alert.html', context)
        plain_message = render_to_string('emails/security_alert.txt', context)
        
        email = EmailMultiAlternatives(
            subject=subject,
            body=plain_message,
            from_email=settings.DEFAULT_FROM_EMAIL,
            to=[user.email],
        )
        email.attach_alternative(html_message, "text/html")
        email.send(fail_silently=False)
        
        logger.info(f"Security alert email sent to {user.email} for {event_type}")
        return True
        
    except Exception as e:
        logger.error(f"Failed to send security alert email: {str(e)}")
        return False

def validate_otp_rate_limit(user, ip_address):
    """Check if user/IP can request new OTP"""
    from .models import EmailOTP
    
    # Check user-specific rate limit
    if hasattr(user, 'profile') and not user.profile.can_request_otp():
        return False, "Please wait before requesting another code"
    
    # Check IP-specific rate limit (5 requests per hour)
    recent_requests = EmailOTP.objects.filter(
        ip_address=ip_address,
        created_at__gte=timezone.now() - timezone.timedelta(hours=1)
    ).count()
    
    if recent_requests >= 5:
        return False, "Too many requests from this IP address"
    
    return True, ""

def cleanup_expired_otps():
    """Cleanup expired OTP codes (run as periodic task)"""
    from .models import EmailOTP
    
    expired_count = EmailOTP.objects.filter(
        expires_at__lt=timezone.now(),
        status='active'
    ).update(status='expired')
    
    # Delete old OTPs (older than 24 hours)
    old_otps = EmailOTP.objects.filter(
        created_at__lt=timezone.now() - timezone.timedelta(days=1)
    )
    deleted_count = old_otps.count()
    old_otps.delete()
    
    logger.info(f"Cleanup completed: {expired_count} expired, {deleted_count} deleted")
    return expired_count, deleted_count

Email Templates

Create professional email templates:

<!-- templates/emails/otp_email.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Your Verification Code</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            line-height: 1.6;
            color: #333;
            background-color: #f8f9fa;
            margin: 0;
            padding: 20px;
        }
        .container {
            max-width: 600px;
            margin: 0 auto;
            background: white;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            overflow: hidden;
        }
        .header {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 30px;
            text-align: center;
        }
        .content {
            padding: 30px;
        }
        .otp-code {
            background: #f8f9fa;
            border: 2px dashed #dee2e6;
            border-radius: 8px;
            font-size: 32px;
            font-weight: bold;
            text-align: center;
            padding: 20px;
            margin: 20px 0;
            letter-spacing: 8px;
            color: #495057;
        }
        .info-box {
            background: #e3f2fd;
            border-left: 4px solid #2196f3;
            padding: 15px;
            margin: 20px 0;
            border-radius: 4px;
        }
        .footer {
            background: #f8f9fa;
            padding: 20px;
            text-align: center;
            font-size: 12px;
            color: #6c757d;
        }
        .button {
            display: inline-block;
            background: #007bff;
            color: white;
            padding: 12px 24px;
            text-decoration: none;
            border-radius: 5px;
            margin: 10px 0;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>{{ app_name }}</h1>
            <h2>{{ purpose }} Verification</h2>
        </div>
        
        <div class="content">
            <p>Hello {{ user.first_name|default:user.username }},</p>
            
            <p>Your verification code for {{ purpose|lower }} is:</p>
            
            <div class="otp-code">{{ otp_code }}</div>
            
            <div class="info-box">
                <strong>Important:</strong>
                <ul>
                    <li>This code expires at <strong>{{ expires_at|date:"M j, Y g:i A" }}</strong></li>
                    <li>Do not share this code with anyone</li>
                    <li>If you didn't request this, please ignore this email</li>
                </ul>
            </div>
            
            <h3>Security Information</h3>
            <p><strong>IP Address:</strong> {{ ip_address }}</p>
            <p><strong>Device:</strong> {{ device_info }}</p>
            <p><strong>Time:</strong> {{ timestamp|date:"M j, Y g:i A" }}</p>
            
            <p>If you have any concerns about your account security, please contact our support team immediately.</p>
        </div>
        
        <div class="footer">
            <p>This email was sent by {{ app_name }}. If you have questions, contact us at {{ support_email }}.</p>
            <p>&copy; {% now "Y" %} {{ app_name }}. All rights reserved.</p>
        </div>
    </div>
</body>
</html>
<!-- templates/emails/otp_email.txt -->
{{ app_name }} - {{ purpose }} Verification Code

Hello {{ user.first_name|default:user.username }},

Your verification code for {{ purpose|lower }} is: {{ otp_code }}

IMPORTANT INFORMATION:
- This code expires at {{ expires_at|date:"M j, Y g:i A" }}
- Do not share this code with anyone
- If you didn't request this, please ignore this email

SECURITY DETAILS:
- IP Address: {{ ip_address }}
- Device: {{ device_info }}
- Time: {{ timestamp|date:"M j, Y g:i A" }}

If you have any concerns about your account security, please contact our support team immediately at {{ support_email }}.

---
This email was sent by {{ app_name }}.
© {% now "Y" %} {{ app_name }}. All rights reserved.

Authentication Views and Business Logic

Core Authentication Views

Create comprehensive authentication views with proper error handling and security measures:

# accounts/views.py
import json
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
from django.db.models import Q
from django.http import JsonResponse
from django.shortcuts import render, redirect
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from django.views.decorators.cache import never_cache
from django.contrib import messages
from django.utils import timezone
from django.core.exceptions import ValidationError
import logging

from .models import EmailOTP, UserProfile, LoginAttempt, SecurityEvent
from .utils import (
    send_otp_email, get_client_ip, get_user_agent, 
    validate_otp_rate_limit, send_security_alert_email
)

logger = logging.getLogger(__name__)

@never_cache
def login_view(request):
    """Render login page"""
    if request.user.is_authenticated:
        return redirect('dashboard')
    
    context = {
        'title': 'Sign In to Your Account',
        'page_description': 'Secure login with two-factor authentication',
    }
    return render(request, 'accounts/login.html', context)

@csrf_exempt
@require_http_methods(["POST"])
@never_cache
def send_login_otp(request):
    """Handle initial login and send OTP if required"""
    try:
        # Rate limiting check
        ip_address = get_client_ip(request)
        recent_failures = LoginAttempt.get_recent_failures(ip_address)
        
        if recent_failures >= 5:
            return JsonResponse({
                'success': False,
                'error': 'Too many failed attempts. Please try again later.',
                'retry_after': 900  # 15 minutes
            }, status=429)
        
        # Parse request data
        if request.content_type == 'application/json':
            data = json.loads(request.body.decode('utf-8'))
        else:
            data = {
                'username_or_email': request.POST.get('login', '').strip(),
                'password': request.POST.get('password', ''),
                'remember_me': request.POST.get('remember_me') == 'on'
            }
        
        username_or_email = data.get('username_or_email', '').strip()
        password = data.get('password', '')
        
        # Validation
        if not username_or_email or not password:
            LoginAttempt.log_attempt(
                username=username_or_email,
                ip_address=ip_address,
                result='invalid_credentials',
                user_agent=get_user_agent(request),
                session_key=request.session.session_key or ''
            )
            return JsonResponse({
                'success': False, 
                'error': 'Username/email and password are required'
            }, status=400)
        
        # Find user
        user = User.objects.filter(
            Q(username__iexact=username_or_email) | 
            Q(email__iexact=username_or_email)
        ).select_related('profile').first()
        
        # Verify credentials
        if not user or not user.check_password(password):
            LoginAttempt.log_attempt(
                username=username_or_email,
                ip_address=ip_address,
                result='invalid_credentials',
                user=user,
                user_agent=get_user_agent(request),
                session_key=request.session.session_key or ''
            )
            return JsonResponse({
                'success': False, 
                'error': 'Invalid username/email or password'
            }, status=400)
        
        # Check if account is active
        if not user.is_active:
            LoginAttempt.log_attempt(
                username=username_or_email,
                ip_address=ip_address,
                result='account_locked',
                user=user,
                user_agent=get_user_agent(request)
            )
            return JsonResponse({
                'success': False,
                'error': 'Account is disabled. Please contact support.'
            }, status=403)
        
        # Get or create user profile
        profile, created = UserProfile.objects.get_or_create(user=user)
        
        # Check if OTP is required
        if profile.otp_enabled:
            # Validate rate limits
            rate_limit_valid, rate_limit_message = validate_otp_rate_limit(user, ip_address)
            if not rate_limit_valid:
                return JsonResponse({
                    'success': False,
                    'error': rate_limit_message
                }, status=429)
            
            # Generate and send OTP
            user_agent = get_user_agent(request)
            
            try:
                otp = EmailOTP.generate_otp(
                    user=user,
                    purpose='login',
                    ip_address=ip_address,
                    user_agent=user_agent
                )
                
                # Store login session data
                request.session['otp_login_data'] = {
                    'user_id': user.id,
                    'username': username_or_email,
                    'remember_me': data.get('remember_me', False),
                    'timestamp': timezone.now().isoformat(),
                    'ip_address': ip_address
                }
                request.session.set_expiry(600)  # 10 minutes for OTP session
                
                # Send OTP email
                if send_otp_email(user, otp, request):
                    logger.info(f"OTP sent for login: {user.username} from {ip_address}")
                    
                    return JsonResponse({
                        'success': True,
                        'message': f'Verification code sent to {user.email[:3]}***{user.email[-10:]}',
                        'otp_required': True,
                        'expires_in': 600  # 10 minutes
                    })
                else:
                    # Email sending failed
                    return JsonResponse({
                        'success': False,
                        'error': 'Failed to send verification code. Please try again.'
                    }, status=503)
                    
            except Exception as e:
                logger.error(f"OTP generation failed for {user.username}: {str(e)}")
                return JsonResponse({
                    'success': False,
                    'error': 'Unable to process request. Please try again.'
                }, status=500)
        else:
            # Direct login (no OTP required)
            login(request, user)
            
            # Set session expiry
            if data.get('remember_me'):
                request.session.set_expiry(2592000)  # 30 days
            else:
                request.session.set_expiry(0)  # Browser close
            
            # Log successful attempt
            LoginAttempt.log_attempt(
                username=username_or_email,
                ip_address=ip_address,
                result='success',
                user=user,
                user_agent=get_user_agent(request),
                session_key=request.session.session_key
            )
            
            logger.info(f"Direct login successful: {user.username} from {ip_address}")
            
            return JsonResponse({
                'success': True,
                'message': 'Login successful',
                'redirect_url': '/dashboard/'
            })
            
    except json.JSONDecodeError:
        return JsonResponse({
            'success': False,
            'error': 'Invalid request format'
        }, status=400)
        
    except Exception as e:
        logger.error(f"Login error: {str(e)}")
        return JsonResponse({
            'success': False,
            'error': 'An unexpected error occurred. Please try again.'
        }, status=500)

@csrf_exempt
@require_http_methods(["POST"])
@never_cache
def verify_login_otp(request):
    """Verify OTP and complete login process"""
    try:
        # Get session data
        otp_data = request.session.get('otp_login_data')
        if not otp_data:
            return JsonResponse({
                'success': False,
                'error': 'Session expired. Please start login process again.',
                'redirect': '/login/'
            }, status=400)
        
        # Validate session timestamp
        session_time = timezone.datetime.fromisoformat(otp_data['timestamp'])
        if timezone.now() - session_time > timezone.timedelta(minutes=10):
            del request.session['otp_login_data']
            return JsonResponse({
                'success': False,
                'error': 'Session expired. Please start login process again.',
                'redirect': '/login/'
            }, status=400)
        
        # Parse OTP code
        if request.content_type == 'application/json':
            data = json.loads(request.body.decode('utf-8'))
        else:
            data = request.POST
        
        otp_code = data.get('otp_code', '').strip()
        if not otp_code or len(otp_code) != 6 or not otp_code.isdigit():
            return JsonResponse({
                'success': False,
                'error': 'Please enter a valid 6-digit verification code'
            }, status=400)
        
        # Get user and verify OTP
        try:
            user = User.objects.get(id=otp_data['user_id'])
        except User.DoesNotExist:
            del request.session['otp_login_data']
            return JsonResponse({
                'success': False,
                'error': 'Invalid session. Please start login process again.',
                'redirect': '/login/'
            }, status=400)
        
        # Find valid OTP
        otp = EmailOTP.objects.filter(
            user=user,
            purpose='login',
            status='active'
        ).order_by('-created_at').first()
        
        if not otp:
            LoginAttempt.log_attempt(
                username=otp_data['username'],
                ip_address=get_client_ip(request),
                result='invalid_otp',
                user=user,
                user_agent=get_user_agent(request),
                details={'reason': 'no_active_otp'}
            )
            return JsonResponse({
                'success': False,
                'error': 'No valid verification code found. Please request a new one.',
                'redirect': '/login/'
            }, status=400)
        
        # Verify OTP
        if otp.verify(otp_code):
            # Complete login
            login(request, user)
            
            # Set session expiry based on remember me
            if otp_data.get('remember_me'):
                request.session.set_expiry(2592000)  # 30 days
            else:
                request.session.set_expiry(0)  # Browser close
            
            # Log successful login
            LoginAttempt.log_attempt(
                username=otp_data['username'],
                ip_address=get_client_ip(request),
                result='success',
                user=user,
                user_agent=get_user_agent(request),
                session_key=request.session.session_key,
                details={'otp_verified': True}
            )
            
            # Clear OTP session data
            del request.session['otp_login_data']
            
            logger.info(f"OTP login successful: {user.username} from {get_client_ip(request)}")
            
            return JsonResponse({
                'success': True,
                'message': 'Login successful',
                'redirect_url': '/dashboard/'
            })
        else:
            # OTP verification failed
            ip_address = get_client_ip(request)
            
            LoginAttempt.log_attempt(
                username=otp_data['username'],
                ip_address=ip_address,
                result='invalid_otp',
                user=user,
                user_agent=get_user_agent(request),
                details={'attempts': otp.attempts}
            )
            
            # Check if maximum attempts reached
            if otp.attempts >= otp.max_attempts:
                return JsonResponse({
                    'success': False,
                    'error': 'Maximum verification attempts exceeded. Please request a new code.',
                    'max_attempts_reached': True
                }, status=400)
            
            remaining_attempts = otp.max_attempts - otp.attempts
            return JsonResponse({
                'success': False,
                'error': f'Invalid verification code. {remaining_attempts} attempts remaining.',
                'remaining_attempts': remaining_attempts
            }, status=400)
            
    except json.JSONDecodeError:
        return JsonResponse({
            'success': False,
            'error': 'Invalid request format'
        }, status=400)
        
    except Exception as e:
        logger.error(f"OTP verification error: {str(e)}")
        return JsonResponse({
            'success': False,
            'error': 'Verification failed. Please try again.'
        }, status=500)

@csrf_exempt
@require_http_methods(["POST"])
@never_cache
def resend_otp(request):
    """Resend OTP code"""
    try:
        # Check session
        otp_data = request.session.get('otp_login_data')
        if not otp_data:
            return JsonResponse({
                'success': False,
                'error': 'No active OTP session found'
            }, status=400)
        
        # Get user
        user = User.objects.get(id=otp_data['user_id'])
        ip_address = get_client_ip(request)
        
        # Rate limiting
        rate_limit_valid, rate_limit_message = validate_otp_rate_limit(user, ip_address)
        if not rate_limit_valid:
            return JsonResponse({
                'success': False,
                'error': rate_limit_message
            }, status=429)
        
        # Generate new OTP
        otp = EmailOTP.generate_otp(
            user=user,
            purpose='login',
            ip_address=ip_address,
            user_agent=get_user_agent(request)
        )
        
        # Send email
        if send_otp_email(user, otp, request):
            logger.info(f"OTP resent for: {user.username} from {ip_address}")
            return JsonResponse({
                'success': True,
                'message': 'New verification code sent',
                'expires_in': 600
            })
        else:
            return JsonResponse({
                'success': False,
                'error': 'Failed to send verification code'
            }, status=503)
            
    except Exception as e:
        logger.error(f"OTP resend error: {str(e)}")
        return JsonResponse({
            'success': False,
            'error': 'Failed to resend code'
        }, status=500)

@login_required
@never_cache
def dashboard_view(request):
    """User dashboard"""
    context = {
        'title': 'Dashboard',
        'user': request.user,
        'otp_enabled': getattr(request.user.profile, 'otp_enabled', False),
        'recent_logins': LoginAttempt.objects.filter(
            user=request.user,
            result='success'
        )[:5]
    }
    return render(request, 'accounts/dashboard.html', context)

@login_required
def logout_view(request):
    """Logout user"""
    user = request.user
    logout(request)
    messages.success(request, 'You have been logged out successfully.')
    logger.info(f"User logged out: {user.username}")
    return redirect('login')

@login_required
@csrf_exempt
@require_http_methods(["POST"])
def toggle_otp(request):
    """Enable or disable OTP for user account"""
    try:
        profile, created = UserProfile.objects.get_or_create(user=request.user)
        
        if request.content_type == 'application/json':
            data = json.loads(request.body.decode('utf-8'))
        else:
            data = request.POST
        
        enable_otp = data.get('enable_otp', False)
        
        if enable_otp and not profile.otp_enabled:
            # Enabling OTP - send verification email first
            ip_address = get_client_ip(request)
            
            otp = EmailOTP.generate_otp(
                user=request.user,
                purpose='enable_2fa',
                ip_address=ip_address,
                user_agent=get_user_agent(request)
            )
            
            if send_otp_email(request.user, otp, request):
                # Store pending change in session
                request.session['pending_otp_enable'] = True
                
                return JsonResponse({
                    'success': True,
                    'message': 'Verification code sent to your email',
                    'verification_required': True
                })
            else:
                return JsonResponse({
                    'success': False,
                    'error': 'Failed to send verification email'
                }, status=503)
                
        elif not enable_otp and profile.otp_enabled:
            # Disabling OTP
            profile.otp_enabled = False
            profile.save()
            
            # Log security event
            SecurityEvent.objects.create(
                user=request.user,
                event_type='otp_disabled',
                description='Two-factor authentication disabled',
                ip_address=get_client_ip(request),
                user_agent=get_user_agent(request)
            )
            
            # Send security alert
            send_security_alert_email(
                request.user,
                'OTP Disabled',
                'Two-factor authentication has been disabled for your account',
                request
            )
            
            logger.info(f"OTP disabled for user: {request.user.username}")
            
            return JsonResponse({
                'success': True,
                'message': 'Two-factor authentication disabled',
                'otp_enabled': False
            })
        
        return JsonResponse({
            'success': True,
            'message': 'No changes made',
            'otp_enabled': profile.otp_enabled
        })
        
    except Exception as e:
        logger.error(f"Toggle OTP error: {str(e)}")
        return JsonResponse({
            'success': False,
            'error': 'Failed to update settings'
        }, status=500)

@login_required
@csrf_exempt
@require_http_methods(["POST"])
def confirm_enable_otp(request):
    """Confirm OTP enabling with verification code"""
    try:
        if not request.session.get('pending_otp_enable'):
            return JsonResponse({
                'success': False,
                'error': 'No pending OTP enable request'
            }, status=400)
        
        if request.content_type == 'application/json':
            data = json.loads(request.body.decode('utf-8'))
        else:
            data = request.POST
        
        otp_code = data.get('otp_code', '').strip()
        
        if not otp_code or len(otp_code) != 6:
            return JsonResponse({
                'success': False,
                'error': 'Please enter a valid 6-digit code'
            }, status=400)
        
        # Find and verify OTP
        otp = EmailOTP.objects.filter(
            user=request.user,
            purpose='enable_2fa',
            status='active'
        ).order_by('-created_at').first()
        
        if not otp or not otp.verify(otp_code):
            return JsonResponse({
                'success': False,
                'error': 'Invalid or expired verification code'
            }, status=400)
        
        # Enable OTP
        profile, created = UserProfile.objects.get_or_create(user=request.user)
        profile.otp_enabled = True
        profile.save()
        
        # Generate backup codes
        backup_codes = profile.generate_backup_codes()
        
        # Clear pending session
        del request.session['pending_otp_enable']
        
        # Log security event
        SecurityEvent.objects.create(
            user=request.user,
            event_type='otp_enabled',
            description='Two-factor authentication enabled',
            ip_address=get_client_ip(request),
            user_agent=get_user_agent(request)
        )
        
        logger.info(f"OTP enabled for user: {request.user.username}")
        
        return JsonResponse({
            'success': True,
            'message': 'Two-factor authentication enabled successfully',
            'otp_enabled': True,
            'backup_codes': backup_codes
        })
        
    except Exception as e:
        logger.error(f"Confirm enable OTP error: {str(e)}")
        return JsonResponse({
            'success': False,
            'error': 'Failed to enable two-factor authentication'
        }, status=500)

URL Configuration

Configure URL patterns:

# accounts/urls.py
from django.urls import path
from . import views

app_name = 'accounts'

urlpatterns = [
    # Authentication pages
    path('login/', views.login_view, name='login'),
    path('logout/', views.logout_view, name='logout'),
    path('dashboard/', views.dashboard_view, name='dashboard'),
    
    # API endpoints
    path('api/send-login-otp/', views.send_login_otp, name='send_login_otp'),
    path('api/verify-login-otp/', views.verify_login_otp, name='verify_login_otp'),
    path('api/resend-otp/', views.resend_otp, name='resend_otp'),
    path('api/toggle-otp/', views.toggle_otp, name='toggle_otp'),
    path('api/confirm-enable-otp/', views.confirm_enable_otp, name='confirm_enable_otp'),
]
# otpauth/urls.py
from django.contrib import admin
from django.urls import path, include
from django.shortcuts import redirect

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('accounts.urls')),
    path('', lambda request: redirect('accounts:login')),
]

Frontend Implementation with TailwindCSS

Base Template

Create a professional base template:

<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="{% block meta_description %}Secure authentication system with two-factor authentication{% endblock %}">
    <meta name="robots" content="noindex, nofollow">
    <title>{% block title %}{{ title|default:'Django OTP Auth' }}{% endblock %}</title>
    
    <!-- TailwindCSS -->
    <script src="https://cdn.tailwindcss.com"></script>
    
    <!-- Custom styles -->
    <style>
        .gradient-bg {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        }
        .glass-effect {
            background: rgba(255, 255, 255, 0.25);
            backdrop-filter: blur(10px);
            border: 1px solid rgba(255, 255, 255, 0.18);
        }
        .otp-input {
            transition: all 0.3s ease;
        }
        .otp-input:focus {
            transform: scale(1.05);
            box-shadow: 0 0 20px rgba(102, 126, 234, 0.4);
        }
        .animate-pulse-slow {
            animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
        }
        .floating-animation {
            animation: floating 6s ease-in-out infinite;
        }
        @keyframes floating {
            0%, 100% { transform: translateY(0px); }
            50% { transform: translateY(-20px); }
        }
        .typing-animation {
            overflow: hidden;
            border-right: 2px solid rgba(255,255,255,.75);
            white-space: nowrap;
            margin: 0 auto;
            letter-spacing: .1em;
            animation: typing 3.5s steps(40, end), blink-caret .75s step-end infinite;
        }
        @keyframes typing {
            from { width: 0 }
            to { width: 100% }
        }
        @keyframes blink-caret {
            from, to { border-color: transparent }
            50% { border-color: rgba(255,255,255,.75); }
        }
    </style>
    
    <!-- Favicon -->
    <link rel="icon" type="image/x-icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🔐</text></svg>">
    
    {% block extra_head %}{% endblock %}
</head>
<body class="h-full {% block body_class %}bg-gray-900{% endblock %}">
    <!-- Loading spinner -->
    <div id="loading" class="fixed inset-0 bg-gray-900 bg-opacity-75 flex items-center justify-center z-50 hidden">
        <div class="animate-spin rounded-full h-32 w-32 border-b-2 border-white"></div>
    </div>
    
    <!-- Flash messages -->
    {% if messages %}
        <div id="flash-messages" class="fixed top-4 right-4 z-40 space-y-2">
            {% for message in messages %}
                <div class="bg-{{ message.tags == 'error' and 'red' or 'green' }}-500 text-white px-6 py-3 rounded-lg shadow-lg max-w-sm
                            transform transition-all duration-300 ease-in-out" 
                     x-data="{ show: true }" 
                     x-show="show" 
                     x-transition:enter="transition ease-out duration-300"
                     x-transition:enter-start="opacity-0 transform translate-x-full"
                     x-transition:enter-end="opacity-100 transform translate-x-0"
                     x-transition:leave="transition ease-in duration-300"
                     x-transition:leave-start="opacity-100 transform translate-x-0"
                     x-transition:leave-end="opacity-0 transform translate-x-full"
                     x-init="setTimeout(() => show = false, 5000)">
                    <div class="flex items-center justify-between">
                        <span>{{ message }}</span>
                        <button @click="show = false" class="ml-4 text-white hover:text-gray-200">
                            <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
                            </svg>
                        </button>
                    </div>
                </div>
            {% endfor %}
        </div>
    {% endif %}
    
    {% block content %}{% endblock %}
    
    <!-- Alpine.js for interactive components -->
    <script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
    
    <!-- Global JavaScript utilities -->
    <script>
        // CSRF Token utility
        function getCSRFToken() {
            return document.querySelector('[name=csrfmiddlewaretoken]')?.value || '';
        }
        
        // Show loading spinner
        function showLoading() {
            document.getElementById('loading').classList.remove('hidden');
        }
        
        // Hide loading spinner
        function hideLoading() {
            document.getElementById('loading').classList.add('hidden');
        }
        
        // Show toast notification
        function showToast(message, type = 'success') {
            const toast = document.createElement('div');
            toast.className = `fixed top-4 right-4 z-50 px-6 py-3 rounded-lg shadow-lg max-w-sm transform transition-all duration-300 ${
                type === 'error' ? 'bg-red-500' : 'bg-green-500'
            } text-white`;
            toast.textContent = message;
            
            document.body.appendChild(toast);
            
            // Animate in
            setTimeout(() => {
                toast.style.transform = 'translateX(0)';
            }, 100);
            
            // Remove after 5 seconds
            setTimeout(() => {
                toast.style.transform = 'translateX(100%)';
                setTimeout(() => toast.remove(), 300);
            }, 5000);
        }
        
        // API request utility
        async function apiRequest(url, data = {}, method = 'POST') {
            try {
                showLoading();
                
                const response = await fetch(url, {
                    method: method,
                    headers: {
                        'Content-Type': 'application/json',
                        'X-CSRFToken': getCSRFToken()
                    },
                    body: method !== 'GET' ? JSON.stringify(data) : null
                });
                
                const result = await response.json();
                return { success: response.ok, data: result, status: response.status };
                
            } catch (error) {
                console.error('API request failed:', error);
                return { success: false, data: { error: 'Network error occurred' }, status: 0 };
            } finally {
                hideLoading();
            }
        }
    </script>
    
    {% block extra_js %}{% endblock %}
</body>
</html>

Login Page Template

Create a stunning, responsive login interface:

<!-- templates/accounts/login.html -->
{% extends 'base.html' %}

{% block title %}Sign In - Secure Authentication{% endblock %}
{% block meta_description %}Sign in to your account with secure two-factor authentication. Professional Django OTP login system.{% endblock %}

{% block body_class %}gradient-bg min-h-screen{% endblock %}

{% block content %}
<div class="min-h-screen flex items-center justify-center px-4 sm:px-6 lg:px-8 relative overflow-hidden">
    <!-- Animated background elements -->
    <div class="absolute inset-0">
        <div class="absolute top-10 left-10 w-72 h-72 bg-purple-300 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-pulse-slow floating-animation"></div>
        <div class="absolute top-0 right-4 w-72 h-72 bg-yellow-300 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-pulse-slow floating-animation" style="animation-delay: 2s;"></div>
        <div class="absolute -bottom-8 left-20 w-72 h-72 bg-pink-300 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-pulse-slow floating-animation" style="animation-delay: 4s;"></div>
    </div>
    
    <div class="max-w-md w-full space-y-8 relative z-10">
        <!-- Header -->
        <div class="text-center">
            <div class="mx-auto h-16 w-16 bg-white rounded-full flex items-center justify-center shadow-lg mb-6">
                <svg class="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 
                          d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
                </svg>
            </div>
            <h2 class="text-3xl font-extrabold text-white mb-2 typing-animation">
                Sign in to your account
            </h2>
            <p class="text-purple-100">Enter your credentials to access your dashboard</p>
        </div>

        <!-- Login Form -->
        <div id="loginCard" class="glass-effect rounded-2xl shadow-2xl p-8 transform transition-all duration-500 hover:scale-105">
            <form id="loginForm" class="space-y-6">
                {% csrf_token %}
                
                <!-- Username/Email Field -->
                <div class="relative">
                    <label for="login" class="sr-only">Username or Email</label>
                    <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
                        <svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 
                                  d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207"></path>
                        </svg>
                    </div>
                    <input id="login" name="login" type="text" required 
                           class="appearance-none relative block w-full px-12 py-4 border-0 placeholder-gray-500 text-gray-900 rounded-xl 
                                  focus:outline-none focus:ring-4 focus:ring-purple-400 focus:border-transparent focus:z-10 
                                  bg-white bg-opacity-90 backdrop-blur-sm transition-all duration-300"
                           placeholder="Username or Email Address">
                </div>

                <!-- Password Field -->
                <div class="relative">
                    <label for="password" class="sr-only">Password</label>
                    <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
                        <svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 
                                  d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
                        </svg>
                    </div>
                    <input id="password" name="password" type="password" required 
                           class="appearance-none relative block w-full px-12 py-4 border-0 placeholder-gray-500 text-gray-900 rounded-xl 
                                  focus:outline-none focus:ring-4 focus:ring-purple-400 focus:border-transparent focus:z-10 
                                  bg-white bg-opacity-90 backdrop-blur-sm transition-all duration-300"
                           placeholder="Password">
                    <button type="button" id="togglePassword" 
                            class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600">
                        <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 
                                  d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 
                                  d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
                        </svg>
                    </button>
                </div>

                <!-- Remember Me -->
                <div class="flex items-center justify-between">
                    <div class="flex items-center">
                        <input id="remember_me" name="remember_me" type="checkbox" 
                               class="h-4 w-4 text-purple-600 focus:ring-purple-500 border-gray-300 rounded 
                                      bg-white bg-opacity-90">
                        <label for="remember_me" class="ml-2 block text-sm text-white">
                            Remember me for 30 days
                        </label>
                    </div>
                    <div class="text-sm">
                        <a href="#" class="font-medium text-purple-200 hover:text-white transition-colors duration-300">
                            Forgot your password?
                        </a>
                    </div>
                </div>

                <!-- Submit Button -->
                <div>
                    <button type="submit" id="loginButton"
                            class="group relative w-full flex justify-center py-4 px-4 border border-transparent text-sm font-medium 
                                   rounded-xl text-white bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 
                                   focus:outline-none focus:ring-4 focus:ring-purple-400 transition-all duration-300 
                                   transform hover:scale-105 active:scale-95 shadow-lg hover:shadow-xl">
                        <span class="absolute left-0 inset-y-0 flex items-center pl-3">
                            <svg class="h-5 w-5 text-purple-300 group-hover:text-purple-200 transition-colors duration-300" 
                                 fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 
                                      d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"></path>
                            </svg>
                        </span>
                        <span id="loginButtonText">Sign In</span>
                        <div id="loginSpinner" class="hidden ml-2">
                            <div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
                        </div>
                    </button>
                </div>

                <!-- Error Display -->
                <div id="errorMessage" class="hidden text-red-300 text-sm text-center bg-red-500 bg-opacity-20 p-3 rounded-lg border border-red-300">
                </div>
            </form>
        </div>

        <!-- OTP Form (Hidden by default) -->
        <div id="otpCard" class="hidden glass-effect rounded-2xl shadow-2xl p-8 transform transition-all duration-500">
            <div class="text-center mb-6">
                <div class="mx-auto h-16 w-16 bg-green-500 rounded-full flex items-center justify-center shadow-lg mb-4">
                    <svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 
                              d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
                    </svg>
                </div>
                <h3 class="text-2xl font-bold text-white mb-2">Check Your Email</h3>
                <p class="text-purple-100" id="otpEmailMessage">We've sent a verification code to your email</p>
            </div>

            <!-- OTP Input Fields -->
            <div class="flex justify-center space-x-3 mb-6">
                <input type="text" maxlength="1" 
                       class="otp-input w-14 h-14 text-center text-2xl font-bold border-2 border-white border-opacity-30 
                              rounded-xl bg-white bg-opacity-20 text-white placeholder-purple-200 
                              focus:outline-none focus:border-yellow-400 focus:bg-opacity-30 transition-all duration-300" 
                       id="otp1" autocomplete="off">
                <input type="text" maxlength="1" 
                       class="otp-input w-14 h-14 text-center text-2xl font-bold border-2 border-white border-opacity-30 
                              rounded-xl bg-white bg-opacity-20 text-white placeholder-purple-200 
                              focus:outline-none focus:border-yellow-400 focus:bg-opacity-30 transition-all duration-300" 
                       id="otp2" autocomplete="off">
                <input type="text" maxlength="1" 
                       class="otp-input w-14 h-14 text-center text-2xl font-bold border-2 border-white border-opacity-30 
                              rounded-xl bg-white bg-opacity-20 text-white placeholder-purple-200 
                              focus:outline-none focus:border-yellow-400 focus:bg-opacity-30 transition-all duration-300" 
                       id="otp3" autocomplete="off">
                <input type="text" maxlength="1" 
                       class="otp-input w-14 h-14 text-center text-2xl font-bold border-2 border-white border-opacity-30 
                              rounded-xl bg-white bg-opacity-20 text-white placeholder-purple-200 
                              focus:outline-none focus:border-yellow-400 focus:bg-opacity-30 transition-all duration-300" 
                       id="otp4" autocomplete="off">
                <input type="text" maxlength="1" 
                       class="otp-input w-14 h-14 text-center text-2xl font-bold border-2 border-white border-opacity-30 
                              rounded-xl bg-white bg-opacity-20 text-white placeholder-purple-200 
                              focus:outline-none focus:border-yellow-400 focus:bg-opacity-30 transition-all duration-300" 
                       id="otp5" autocomplete="off">
                <input type="text" maxlength="1" 
                       class="otp-input w-14 h-14 text-center text-2xl font-bold border-2 border-white border-opacity-30 
                              rounded-xl bg-white bg-opacity-20 text-white placeholder-purple-200 
                              focus:outline-none focus:border-yellow-400 focus:bg-opacity-30 transition-all duration-300" 
                       id="otp6" autocomplete="off">
            </div>

            <!-- Timer Display -->
            <div class="text-center mb-4">
                <p class="text-purple-200">Code expires in <span id="timer" class="font-bold text-yellow-300">10:00</span></p>
            </div>

            <!-- Action Buttons -->
            <div class="space-y-3">
                <button id="verifyOtpButton"
                        class="w-full flex justify-center py-4 px-4 border border-transparent text-sm font-medium 
                               rounded-xl text-white bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 
                               focus:outline-none focus:ring-4 focus:ring-green-400 transition-all duration-300 
                               transform hover:scale-105 active:scale-95 shadow-lg hover:shadow-xl">
                    <span id="verifyButtonText">Verify & Sign In</span>
                    <div id="verifySpinner" class="hidden ml-2">
                        <div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
                    </div>
                </button>
                
                <button id="resendOtpButton"
                        class="w-full flex justify-center py-3 px-4 border border-white border-opacity-30 text-sm font-medium 
                               rounded-xl text-white bg-transparent hover:bg-white hover:bg-opacity-10 
                               focus:outline-none focus:ring-4 focus:ring-purple-400 transition-all duration-300">
                    <span id="resendButtonText">Resend Code</span>
                    <div id="resendSpinner" class="hidden ml-2">
                        <div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
                    </div>
                </button>
                
                <button id="backToLoginButton"
                        class="w-full text-center py-3 text-sm text-purple-200 hover:text-white transition-colors duration-300">
                    ← Back to Login
                </button>
            </div>

            <!-- OTP Error Display -->
            <div id="otpErrorMessage" class="hidden mt-4 text-red-300 text-sm text-center bg-red-500 bg-opacity-20 p-3 rounded-lg border border-red-300">
            </div>
        </div>

        <!-- Footer -->
        <div class="text-center text-purple-200 text-sm">
            <p>© 2025 Django OTP Auth. Secure authentication system.</p>
            <p class="mt-1">Don't have an account? <a href="#" class="text-yellow-300 hover:text-yellow-200 transition-colors duration-300">Sign up here</a></p>
        </div>
    </div>
</div>

<!-- Custom JavaScript -->
<script>
document.addEventListener('DOMContentLoaded', function() {
    // Form elements
    const loginForm = document.getElementById('loginForm');
    const loginCard = document.getElementById('loginCard');
    const otpCard = document.getElementById('otpCard');
    const errorMessage = document.getElementById('errorMessage');
    const otpErrorMessage = document.getElementById('otpErrorMessage');
    const loginButton = document.getElementById('loginButton');
    const loginButtonText = document.getElementById('loginButtonText');
    const loginSpinner = document.getElementById('loginSpinner');
    const verifyOtpButton = document.getElementById('verifyOtpButton');
    const verifyButtonText = document.getElementById('verifyButtonText');
    const verifySpinner = document.getElementById('verifySpinner');
    const resendOtpButton = document.getElementById('resendOtpButton');
    const backToLoginButton = document.getElementById('backToLoginButton');
    const togglePassword = document.getElementById('togglePassword');
    const passwordField = document.getElementById('password');
    const timerElement = document.getElementById('timer');
    
    let countdownTimer;
    let resendCooldown = false;

    // Password visibility toggle
    togglePassword.addEventListener('click', function() {
        const type = passwordField.getAttribute('type') === 'password' ? 'text' : 'password';
        passwordField.setAttribute('type', type);
        
        const eyeIcon = this.querySelector('svg');
        if (type === 'text') {
            eyeIcon.innerHTML = `
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21"></path>
            `;
        } else {
            eyeIcon.innerHTML = `
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
            `;
        }
    });

    // Handle login form submission
    loginForm.addEventListener('submit', async function(e) {
        e.preventDefault();
        hideError();
        
        const formData = new FormData(this);
        const loginData = {
            username_or_email: formData.get('login'),
            password: formData.get('password'),
            remember_me: formData.get('remember_me') === 'on'
        };

        setLoginButtonLoading(true);
        
        try {
            const response = await fetch('/accounts/api/send-login-otp/', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRFToken': getCSRFToken()
                },
                body: JSON.stringify(loginData)
            });

            const result = await response.json();

            if (result.success) {
                if (result.otp_required) {
                    showOTPForm(result.message);
                    startTimer(600); // 10 minutes
                } else {
                    window.location.href = result.redirect_url;
                }
            } else {
                showError(result.error);
            }
        } catch (error) {
            console.error('Login error:', error);
            showError('Network error. Please try again.');
        } finally {
            setLoginButtonLoading(false);
        }
    });

    // OTP input handling
    const otpInputs = document.querySelectorAll('.otp-input');
    
    otpInputs.forEach((input, index) => {
        input.addEventListener('input', function(e) {
            const value = e.target.value;
            
            // Only allow digits
            if (!/^\d*$/.test(value)) {
                e.target.value = value.replace(/\D/g, '');
                return;
            }

            // Move to next input
            if (value.length === 1 && index < otpInputs.length - 1) {
                otpInputs[index + 1].focus();
            }

            // Auto-submit when all fields filled
            if (index === otpInputs.length - 1 && value.length === 1) {
                const allFilled = Array.from(otpInputs).every(input => input.value.length === 1);
                if (allFilled) {
                    setTimeout(() => verifyOTP(), 100);
                }
            }
        });

        input.addEventListener('keydown', function(e) {
            // Handle backspace
            if (e.key === 'Backspace' && this.value === '' && index > 0) {
                otpInputs[index - 1].focus();
                otpInputs[index - 1].value = '';
            }
            
            // Handle paste
            if (e.key === 'v' && (e.ctrlKey || e.metaKey)) {
                e.preventDefault();
                navigator.clipboard.readText().then(text => {
                    const digits = text.replace(/\D/g, '').slice(0, 6);
                    digits.split('').forEach((digit, i) => {
                        if (otpInputs[i]) otpInputs[i].value = digit;
                    });
                    if (digits.length === 6) verifyOTP();
                });
            }
        });

        // Clear on focus if all fields filled incorrectly
        input.addEventListener('focus', function() {
            const allFilled = Array.from(otpInputs).every(input => input.value.length === 1);
            if (allFilled && otpErrorMessage && !otpErrorMessage.classList.contains('hidden')) {
                otpInputs.forEach(inp => inp.value = '');
                hideOtpError();
            }
        });
    });

    // Verify OTP button
    verifyOtpButton.addEventListener('click', verifyOTP);

    // Resend OTP button
    resendOtpButton.addEventListener('click', async function() {
        if (resendCooldown) return;
        
        setResendButtonLoading(true);
        resendCooldown = true;
        
        try {
            const response = await fetch('/accounts/api/resend-otp/', {
                method: 'POST',
                headers: {
                    'X-CSRFToken': getCSRFToken()
                }
            });

            const result = await response.json();
            
            if (result.success) {
                showToast('New verification code sent');
                startTimer(600);
                hideOtpError();
            } else {
                showOtpError(result.error);
            }
        } catch (error) {
            console.error('Resend error:', error);
            showOtpError('Failed to resend code');
        } finally {
            setResendButtonLoading(false);
            setTimeout(() => { resendCooldown = false; }, 60000); // 1 minute cooldown
        }
    });

    // Back to login button
    backToLoginButton.addEventListener('click', function() {
        showLoginForm();
        clearOtpInputs();
        hideOtpError();
        if (countdownTimer) {
            clearInterval(countdownTimer);
        }
    });

    // Verify OTP function
    async function verifyOTP() {
        const otpCode = Array.from(otpInputs).map(input => input.value).join('');
        
        if (otpCode.length !== 6) {
            showOtpError('Please enter all 6 digits');
            return;
        }

        hideOtpError();
        setVerifyButtonLoading(true);

        try {
            const response = await fetch('/accounts/api/verify-login-otp/', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRFToken': getCSRFToken()
                },
                body: JSON.stringify({ otp_code: otpCode })
            });

            const result = await response.json();

            if (result.success) {
                showToast('Login successful');
                window.location.href = result.redirect_url;
            } else {
                showOtpError(result.error);
                
                // Clear inputs if max attempts reached
                if (result.max_attempts_reached) {
                    clearOtpInputs();
                    setTimeout(() => {
                        showLoginForm();
                    }, 3000);
                } else {
                    // Focus first input for retry
                    otpInputs[0].focus();
                }
            }
        } catch (error) {
            console.error('OTP verification error:', error);
            showOtpError('Verification failed. Please try again.');
        } finally {
            setVerifyButtonLoading(false);
        }
    }

    // Timer function
    function startTimer(seconds) {
        let timeLeft = seconds;
        
        if (countdownTimer) {
            clearInterval(countdownTimer);
        }
        
        countdownTimer = setInterval(() => {
            const minutes = Math.floor(timeLeft / 60);
            const secs = timeLeft % 60;
            
            timerElement.textContent = `${minutes}:${secs.toString().padStart(2, '0')}`;
            
            if (timeLeft <= 0) {
                clearInterval(countdownTimer);
                timerElement.textContent = 'Expired';
                showOtpError('Verification code has expired. Please request a new one.');
            }
            
            timeLeft--;
        }, 1000);
    }

    // UI Helper Functions
    function showLoginForm() {
        loginCard.classList.remove('hidden');
        otpCard.classList.add('hidden');
        
        // Animate transition
        setTimeout(() => {
            loginCard.style.transform = 'translateY(0) scale(1)';
            loginCard.style.opacity = '1';
        }, 100);
    }

    function showOTPForm(message) {
        loginCard.classList.add('hidden');
        otpCard.classList.remove('hidden');
        
        if (message) {
            document.getElementById('otpEmailMessage').textContent = message;
        }
        
        // Focus first OTP input
        setTimeout(() => {
            otpInputs[0].focus();
            otpCard.style.transform = 'translateY(0) scale(1)';
            otpCard.style.opacity = '1';
        }, 100);
    }

    function showError(message) {
        errorMessage.textContent = message;
        errorMessage.classList.remove('hidden');
        errorMessage.style.animation = 'shake 0.6s ease-in-out';
    }

    function hideError() {
        errorMessage.classList.add('hidden');
        errorMessage.style.animation = '';
    }

    function showOtpError(message) {
        otpErrorMessage.textContent = message;
        otpErrorMessage.classList.remove('hidden');
        otpErrorMessage.style.animation = 'shake 0.6s ease-in-out';
    }

    function hideOtpError() {
        otpErrorMessage.classList.add('hidden');
        otpErrorMessage.style.animation = '';
    }

    function clearOtpInputs() {
        otpInputs.forEach(input => input.value = '');
    }

    function setLoginButtonLoading(loading) {
        if (loading) {
            loginButtonText.textContent = 'Signing In...';
            loginSpinner.classList.remove('hidden');
            loginButton.disabled = true;
        } else {
            loginButtonText.textContent = 'Sign In';
            loginSpinner.classList.add('hidden');
            loginButton.disabled = false;
        }
    }

    function setVerifyButtonLoading(loading) {
        if (loading) {
            verifyButtonText.textContent = 'Verifying...';
            verifySpinner.classList.remove('hidden');
            verifyOtpButton.disabled = true;
        } else {
            verifyButtonText.textContent = 'Verify & Sign In';
            verifySpinner.classList.add('hidden');
            verifyOtpButton.disabled = false;
        }
    }

    function setResendButtonLoading(loading) {
        const resendButtonText = document.getElementById('resendButtonText');
        const resendSpinner = document.getElementById('resendSpinner');
        
        if (loading) {
            resendButtonText.textContent = 'Sending...';
            resendSpinner.classList.remove('hidden');
            resendOtpButton.disabled = true;
        } else {
            resendButtonText.textContent = 'Resend Code';
            resendSpinner.classList.add('hidden');
            resendOtpButton.disabled = false;
        }
    }

    // Add shake animation CSS
    const style = document.createElement('style');
    style.textContent = `
        @keyframes shake {
            0%, 100% { transform: translateX(0); }
            10%, 30%, 50%, 70%, 90% { transform: translateX(-10px); }
            20%, 40%, 60%, 80% { transform: translateX(10px); }
        }
    `;
    document.head.appendChild(style);
});
</script>
{% endblock %}

Dashboard Template

Create a comprehensive user dashboard:

<!-- templates/accounts/dashboard.html -->
{% extends 'base.html' %}

{% block title %}Dashboard - Your Account{% endblock %}
{% block body_class %}bg-gray-50 min-h-screen{% endblock %}

{% block content %}
<div class="min-h-screen bg-gray-50">
    <!-- Navigation Header -->
    <nav class="bg-white shadow-lg border-b">
        <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
            <div class="flex justify-between items-center h-16">
                <div class="flex items-center">
                    <div class="flex-shrink-0">
                        <div class="h-8 w-8 bg-gradient-to-r from-purple-600 to-blue-600 rounded-lg flex items-center justify-center">
                            <svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 
                                      d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
                            </svg>
                        </div>
                    </div>
                    <div class="ml-4">
                        <h1 class="text-xl font-semibold text-gray-900">Dashboard</h1>
                    </div>
                </div>

                <div class="flex items-center space-x-4">
                    <div class="text-sm text-gray-700">
                        Welcome, <span class="font-medium">{{ user.get_full_name|default:user.username }}</span>
                    </div>
                    <a href="{% url 'accounts:logout' %}" 
                       class="bg-gray-100 hover:bg-gray-200 text-gray-800 px-4 py-2 rounded-lg transition-colors duration-200">
                        Sign Out
                    </a>
                </div>
            </div>
        </div>
    </nav>

    <div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
        <!-- Security Status Card -->
        <div class="bg-white overflow-hidden shadow-xl rounded-lg mb-6">
            <div class="px-6 py-4 border-b border-gray-200">
                <h2 class="text-lg font-medium text-gray-900">Account Security</h2>
                <p class="text-sm text-gray-600">manage your account security settings</p>
            </div>
            <div class="p-6">
                <div class="flex items-center justify-between">
                    <div class="flex items-center">
                        <div class="flex-shrink-0">
                            <div class="h-10 w-10 {{ otp_enabled|yesno:'bg-green-100,bg-yellow-100' }} rounded-full flex items-center justify-center">
                                {% if otp_enabled %}
                                    <svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>
                                    </svg>
                                {% else %}
                                    <svg class="w-6 h-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.664-.833-2.464 0L5.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
                                    </svg>
                                {% endif %}
                            </div>
                        </div>
                        <div class="ml-4">
                            <h3 class="text-sm font-medium text-gray-900">Two-Factor Authentication</h3>
                            <p class="text-sm text-gray-600">
                                {% if otp_enabled %}
                                    Your account is protected with 2FA
                                {% else %}
                                    Enhance your account security
                                {% endif %}
                            </p>
                        </div>
                    </div>
                    <div class="flex items-center space-x-3">
                        <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
                                     {{ otp_enabled|yesno:'bg-green-100 text-green-800,bg-yellow-100 text-yellow-800' }}">
                            {{ otp_enabled|yesno:'Enabled,Disabled' }}
                        </span>
                        <button id="toggleOtpButton" 
                                class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors duration-200"
                                data-enabled="{{ otp_enabled|yesno:'true,false' }}">
                            {{ otp_enabled|yesno:'Disable,Enable' }} 2FA
                        </button>
                    </div>
                </div>
            </div>
        </div>

        <!-- Account Information -->
        <div class="bg-white overflow-hidden shadow-xl rounded-lg mb-6">
            <div class="px-6 py-4 border-b border-gray-200">
                <h2 class="text-lg font-medium text-gray-900">Account Information</h2>
            </div>
            <div class="p-6">
                <dl class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
                    <div>
                        <dt class="text-sm font-medium text-gray-500">Full Name</dt>
                        <dd class="mt-1 text-sm text-gray-900">{{ user.get_full_name|default:'Not provided' }}</dd>
                    </div>
                    <div>
                        <dt class="text-sm font-medium text-gray-500">Username</dt>
                        <dd class="mt-1 text-sm text-gray-900">{{ user.username }}</dd>
                    </div>
                    <div>
                        <dt class="text-sm font-medium text-gray-500">Email Address</dt>
                        <dd class="mt-1 text-sm text-gray-900">{{ user.email }}</dd>
                    </div>
                    <div>
                        <dt class="text-sm font-medium text-gray-500">Account Created</dt>
                        <dd class="mt-1 text-sm text-gray-900">{{ user.date_joined|date:"F j, Y" }}</dd>
                    </div>
                    <div>
                        <dt class="text-sm font-medium text-gray-500">Last Login</dt>
                        <dd class="mt-1 text-sm text-gray-900">{{ user.last_login|date:"F j, Y g:i A"|default:'Never' }}</dd>
                    </div>
                    <div>
                        <dt class="text-sm font-medium text-gray-500">Account Status</dt>
                        <dd class="mt-1">
                            <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
                                         {{ user.is_active|yesno:'bg-green-100 text-green-800,bg-red-100 text-red-800' }}">
                                {{ user.is_active|yesno:'Active,Inactive' }}
                            </span>
                        </dd>
                    </div>
                </dl>
            </div>
        </div>

        <!-- Recent Login Activity -->
        <div class="bg-white overflow-hidden shadow-xl rounded-lg">
            <div class="px-6 py-4 border-b border-gray-200">
                <h2 class="text-lg font-medium text-gray-900">Recent Login Activity</h2>
                <p class="text-sm text-gray-600">Your last 5 successful logins</p>
            </div>
            <div class="overflow-hidden">
                {% if recent_logins %}
                    <ul class="divide-y divide-gray-200">
                        {% for login in recent_logins %}
                            <li class="px-6 py-4">
                                <div class="flex items-center justify-between">
                                    <div class="flex items-center">
                                        <div class="flex-shrink-0">
                                            <div class="h-8 w-8 bg-green-100 rounded-full flex items-center justify-center">
                                                <svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
                                                </svg>
                                            </div>
                                        </div>
                                        <div class="ml-4">
                                            <p class="text-sm font-medium text-gray-900">Successful Login</p>
                                            <p class="text-sm text-gray-600">{{ login.timestamp|date:"F j, Y g:i A" }}</p>
                                        </div>
                                    </div>
                                    <div class="text-sm text-gray-500">
                                        IP: {{ login.ip_address }}
                                    </div>
                                </div>
                            </li>
                        {% endfor %}
                    </ul>
                {% else %}
                    <div class="text-center py-6">
                        <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
                        </svg>
                        <h3 class="mt-2 text-sm font-medium text-gray-900">No login history</h3>
                        <p class="mt-1 text-sm text-gray-500">Your login activity will appear here.</p>
                    </div>
                {% endif %}
            </div>
        </div>
    </div>
</div>

<!-- OTP Enable Modal -->
<div id="otpEnableModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden z-50">
    <div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
        <div class="mt-3 text-center">
            <div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100">
                <svg class="h-6 w-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
                </svg>
            </div>
            <h3 class="text-lg leading-6 font-medium text-gray-900 mt-4">Verify Your Email</h3>
            <div class="mt-2 px-7 py-3">
                <p class="text-sm text-gray-500">Enter the verification code sent to your email to enable 2FA.</p>
            </div>
            <div class="flex justify-center space-x-2 mt-4">
                <input type="text" maxlength="1" class="modal-otp-input w-12 h-12 text-center border rounded focus:outline-none focus:border-green-500" id="modal_otp1">
                <input type="text" maxlength="1" class="modal-otp-input w-12 h-12 text-center border rounded focus:outline-none focus:border-green-500" id="modal_otp2">
                <input type="text" maxlength="1" class="modal-otp-input w-12 h-12 text-center border rounded focus:outline-none focus:border-green-500" id="modal_otp3">
                <input type="text" maxlength="1" class="modal-otp-input w-12 h-12 text-center border rounded focus:outline-none focus:border-green-500" id="modal_otp4">
                <input type="text" maxlength="1" class="modal-otp-input w-12 h-12 text-center border rounded focus:outline-none focus:border-green-500" id="modal_otp5">
                <input type="text" maxlength="1" class="modal-otp-input w-12 h-12 text-center border rounded focus:outline-none focus:border-green-500" id="modal_otp6">
            </div>
            <div id="modalError" class="hidden text-red-600 text-sm mt-2"></div>
            <div class="items-center px-4 py-3 mt-4">
                <button id="confirmOtpButton" class="px-4 py-2 bg-green-500 text-white text-base font-medium rounded-md w-full shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-300">
                    Verify & Enable 2FA
                </button>
                <button id="cancelModalButton" class="mt-2 px-4 py-2 bg-gray-300 text-gray-800 text-base font-medium rounded-md w-full shadow-sm hover:bg-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-300">
                    Cancel
                </button>
            </div>
        </div>
    </div>
</div>

<!-- Backup Codes Modal -->
<div id="backupCodesModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden z-50">
    <div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
        <div class="mt-3 text-center">
            <div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-yellow-100">
                <svg class="h-6 w-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
                </svg>
            </div>
            <h3 class="text-lg leading-6 font-medium text-gray-900 mt-4">Save Your Backup Codes</h3>
            <div class="mt-2 px-7 py-3">
                <p class="text-sm text-gray-500">Save these backup codes in a secure location. You can use them to access your account if you lose access to your email.</p>
            </div>
            <div id="backupCodesList" class="mt-4 text-left bg-gray-100 p-4 rounded font-mono text-sm">
                <!-- Backup codes will be inserted here -->
            </div>
            <div class="items-center px-4 py-3 mt-4">
                <button id="downloadCodesButton" class="px-4 py-2 bg-blue-500 text-white text-base font-medium rounded-md w-full shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-300 mb-2">
                    Download as Text File
                </button>
                <button id="closeBackupModal" class="px-4 py-2 bg-green-500 text-white text-base font-medium rounded-md w-full shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-300">
                    I've Saved These Codes
                </button>
            </div>
        </div>
    </div>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
    const toggleOtpButton = document.getElementById('toggleOtpButton');
    const otpEnableModal = document.getElementById('otpEnableModal');
    const backupCodesModal = document.getElementById('backupCodesModal');
    const modalOtpInputs = document.querySelectorAll('.modal-otp-input');
    const confirmOtpButton = document.getElementById('confirmOtpButton');
    const cancelModalButton = document.getElementById('cancelModalButton');
    const modalError = document.getElementById('modalError');
    const closeBackupModal = document.getElementById('closeBackupModal');
    const downloadCodesButton = document.getElementById('downloadCodesButton');

    let isOtpEnabled = toggleOtpButton.dataset.enabled === 'true';

    // Toggle OTP button click
    toggleOtpButton.addEventListener('click', async function() {
        if (isOtpEnabled) {
            // Disable OTP
            if (confirm('Are you sure you want to disable two-factor authentication? This will make your account less secure.')) {
                await toggleOTP(false);
            }
        } else {
            // Enable OTP - show modal
            await toggleOTP(true);
        }
    });

    // Modal OTP inputs
    modalOtpInputs.forEach((input, index) => {
        input.addEventListener('input', function(e) {
            const value = e.target.value.replace(/\D/g, '');
            e.target.value = value;

            if (value.length === 1 && index < modalOtpInputs.length - 1) {
                modalOtpInputs[index + 1].focus();
            }

            if (index === modalOtpInputs.length - 1 && value.length === 1) {
                const allFilled = Array.from(modalOtpInputs).every(inp => inp.value.length === 1);
                if (allFilled) {
                    setTimeout(() => confirmEnableOTP(), 100);
                }
            }
        });

        input.addEventListener('keydown', function(e) {
            if (e.key === 'Backspace' && this.value === '' && index > 0) {
                modalOtpInputs[index - 1].focus();
            }
        });
    });

    // Confirm OTP button
    confirmOtpButton.addEventListener('click', confirmEnableOTP);

    // Cancel modal
    cancelModalButton.addEventListener('click', function() {
        hideModal(otpEnableModal);
        clearModalOtpInputs();
        hideModalError();
    });

    // Close backup modal
    closeBackupModal.addEventListener('click', function() {
        hideModal(backupCodesModal);
        location.reload(); // Refresh to show updated status
    });

    // Download backup codes
    downloadCodesButton.addEventListener('click', function() {
        const codes = document.getElementById('backupCodesList').textContent;
        const blob = new Blob([codes], { type: 'text/plain' });
        const url = window.URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = 'backup-codes.txt';
        a.click();
        window.URL.revokeObjectURL(url);
    });

    // Toggle OTP function
    async function toggleOTP(enable) {
        try {
            const response = await fetch('/accounts/api/toggle-otp/', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRFToken': getCSRFToken()
                },
                body: JSON.stringify({ enable_otp: enable })
            });

            const result = await response.json();

            if (result.success) {
                if (result.verification_required) {
                    showModal(otpEnableModal);
                    modalOtpInputs[0].focus();
                    showToast('Verification code sent to your email');
                } else {
                    showToast(result.message);
                    updateOTPStatus(result.otp_enabled);
                }
            } else {
                showToast(result.error, 'error');
            }
        } catch (error) {
            console.error('Toggle OTP error:', error);
            showToast('Failed to update settings', 'error');
        }
    }

    // Confirm enable OTP
    async function confirmEnableOTP() {
        const otpCode = Array.from(modalOtpInputs).map(input => input.value).join('');

        if (otpCode.length !== 6) {
            showModalError('Please enter all 6 digits');
            return;
        }

        hideModalError();

        try {
            const response = await fetch('/accounts/api/confirm-enable-otp/', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRFToken': getCSRFToken()
                },
                body: JSON.stringify({ otp_code: otpCode })
            });

            const result = await response.json();

            if (result.success) {
                hideModal(otpEnableModal);
                
                // Show backup codes
                if (result.backup_codes) {
                    showBackupCodes(result.backup_codes);
                }
                
                showToast(result.message);
                updateOTPStatus(result.otp_enabled);
            } else {
                showModalError(result.error);
            }
        } catch (error) {
            console.error('Confirm OTP error:', error);
            showModalError('Verification failed');
        }
    }

    // Show backup codes
    function showBackupCodes(codes) {
        const codesList = document.getElementById('backupCodesList');
        codesList.innerHTML = codes.map((code, index) => 
            `${index + 1}. ${code}`
        ).join('\n');
        
        showModal(backupCodesModal);
    }

    // Update OTP status in UI
    function updateOTPStatus(enabled) {
        isOtpEnabled = enabled;
        toggleOtpButton.dataset.enabled = enabled.toString();
        toggleOtpButton.textContent = enabled ? 'Disable 2FA' : 'Enable 2FA';
        
        // Update status indicators
        const statusElements = document.querySelectorAll('.otp-status');
        statusElements.forEach(el => {
            el.textContent = enabled ? 'Enabled' : 'Disabled';
            el.className = enabled 
                ? 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800'
                : 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800';
        });
    }

    // Modal helper functions
    function showModal(modal) {
        modal.classList.remove('hidden');
        document.body.style.overflow = 'hidden';
    }

    function hideModal(modal) {
        modal.classList.add('hidden');
        document.body.style.overflow = 'auto';
    }

    function showModalError(message) {
        modalError.textContent = message;
        modalError.classList.remove('hidden');
    }

    function hideModalError() {
        modalError.classList.add('hidden');
    }

    function clearModalOtpInputs() {
        modalOtpInputs.forEach(input => input.value = '');
    }
});
</script>
{% endblock %}

Advanced Security Features

Rate Limiting Implementation

Implement comprehensive rate limiting to prevent abuse:

# accounts/middleware.py
import time
from django.core.cache import cache
from django.http import JsonResponse
from django.utils.deprecation import MiddlewareMixin
import logging

logger = logging.getLogger('security')

class RateLimitMiddleware(MiddlewareMixin):
    """Advanced rate limiting middleware"""
    
    def __init__(self, get_response):
        self.get_response = get_response
        super().__init__(get_response)

    def process_request(self, request):
        """Process incoming requests for rate limiting"""
        
        # Skip rate limiting for certain paths
        exempt_paths = ['/admin/', '/static/', '/media/']
        if any(request.path.startswith(path) for path in exempt_paths):
            return None

        # Get client IP
        ip_address = self.get_client_ip(request)
        
        # Different rate limits for different endpoints
        rate_limits = {
            '/accounts/api/send-login-otp/': {'limit': 5, 'window': 300},  # 5 per 5 minutes
            '/accounts/api/verify-login-otp/': {'limit': 10, 'window': 300},  # 10 per 5 minutes
            '/accounts/api/resend-otp/': {'limit': 3, 'window': 600},  # 3 per 10 minutes
        }
        
        # Check if current path needs rate limiting
        for path, limits in rate_limits.items():
            if request.path == path:
                if not self.check_rate_limit(ip_address, path, limits['limit'], limits['window']):
                    logger.warning(f"Rate limit exceeded for {ip_address} on {path}")
                    return JsonResponse({
                        'success': False,
                        'error': 'Rate limit exceeded. Please try again later.',
                        'retry_after': limits['window']
                    }, status=429)
        
        return None

    def get_client_ip(self, request):
        """Get client IP address"""
        x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
        if x_forwarded_for:
            return x_forwarded_for.split(',')[0].strip()
        return request.META.get('REMOTE_ADDR', '127.0.0.1')

    def check_rate_limit(self, identifier, endpoint, limit, window):
        """Check if request is within rate limit"""
        cache_key = f"rate_limit:{endpoint}:{identifier}"
        
        # Get current request count
        current_requests = cache.get(cache_key, [])
        now = time.time()
        
        # Filter requests within the window
        recent_requests = [req_time for req_time in current_requests if now - req_time < window]
        
        # Check if limit exceeded
        if len(recent_requests) >= limit:
            return False
        
        # Add current request and update cache
        recent_requests.append(now)
        cache.set(cache_key, recent_requests, window)
        
        return True

Security Event Logging

Implement comprehensive security event logging:

# accounts/security.py
import logging
import json
from django.utils import timezone
from django.contrib.gis.geoip2 import GeoIP2
from django.core.exceptions import ValidationError
from .models import SecurityEvent, LoginAttempt
from .utils import get_client_ip, get_user_agent

logger = logging.getLogger('security')

class SecurityMonitor:
    """Advanced security monitoring and logging"""
    
    def __init__(self, request=None):
        self.request = request
        self.ip_address = get_client_ip(request) if request else None
        self.user_agent = get_user_agent(request) if request else ''

    def log_security_event(self, user, event_type, description, metadata=None):
        """Log security event with detailed information"""
        try:
            # Get location information (optional)
            location_info = self.get_location_info(self.ip_address)
            
            # Prepare metadata
            event_metadata = {
                'user_agent': self.user_agent,
                'location': location_info,
                'timestamp': timezone.now().isoformat(),
                **(metadata or {})
            }
            
            # Create security event
            SecurityEvent.objects.create(
                user=user,
                event_type=event_type,
                description=description,
                ip_address=self.ip_address or '127.0.0.1',
                user_agent=self.user_agent,
                metadata=event_metadata
            )
            
            # Log to file
            logger.warning(f"Security Event: {event_type} | User: {user.username} | IP: {self.ip_address} | Details: {description}")
            
        except Exception as e:
            logger.error(f"Failed to log security event: {str(e)}")

    def detect_suspicious_activity(self, user):
        """Detect suspicious login patterns"""
        suspicious_indicators = []
        
        # Check for multiple failed attempts
        recent_failures = LoginAttempt.objects.filter(
            user=user,
            result__in=['invalid_credentials', 'invalid_otp'],
            timestamp__gte=timezone.now() - timezone.timedelta(hours=1)
        ).count()
        
        if recent_failures >= 3:
            suspicious_indicators.append(f"Multiple failed attempts: {recent_failures}")
        
        # Check for login from new location
        recent_logins = LoginAttempt.objects.filter(
            user=user,
            result='success',
            timestamp__gte=timezone.now() - timezone.timedelta(days=30)
        ).values_list('ip_address', flat=True).distinct()
        
        if self.ip_address and self.ip_address not in recent_logins and len(recent_logins) > 0:
            suspicious_indicators.append("Login from new IP address")
        
        # Check for unusual timing
        last_login = LoginAttempt.objects.filter(
            user=user,
            result='success'
        ).first()
        
        if last_login:
            time_diff = timezone.now() - last_login.timestamp
            if time_diff.total_seconds() < 300:  # Less than 5 minutes
                suspicious_indicators.append("Very quick successive login")
        
        return suspicious_indicators

    def get_location_info(self, ip_address):
        """Get location information from IP address"""
        try:
            if not ip_address or ip_address in ['127.0.0.1', 'localhost']:
                return {'city': 'Local', 'country': 'Local'}
            
            g = GeoIP2()
            location = g.city(ip_address)
            
            return {
                'city': location.get('city', 'Unknown'),
                'country': location.get('country_name', 'Unknown'),
                'country_code': location.get('country_code', 'Unknown')
            }
        except Exception:
            return {'city': 'Unknown', 'country': 'Unknown'}

    def check_account_lockout(self, user):
        """Check if account should be locked due to suspicious activity"""
        recent_failures = LoginAttempt.objects.filter(
            user=user,
            result__in=['invalid_credentials', 'invalid_otp'],
            timestamp__gte=timezone.now() - timezone.timedelta(hours=1)
        ).count()
        
        if recent_failures >= 10:
            return True, "Too many failed login attempts"
        
        # Check for rapid-fire attempts
        very_recent_attempts = LoginAttempt.objects.filter(
            user=user,
            timestamp__gte=timezone.now() - timezone.timedelta(minutes=5)
        ).count()
        
        if very_recent_attempts >= 5:
            return True, "Rapid successive login attempts"
        
        return False, ""

def monitor_login_attempt(user, request, result, details=None):
    """Monitor and log login attempts with security analysis"""
    monitor = SecurityMonitor(request)
    
    # Log the attempt
    LoginAttempt.log_attempt(
        username=user.username if user else 'unknown',
        ip_address=monitor.ip_address,
        result=result,
        user=user,
        user_agent=monitor.user_agent,
        session_key=request.session.session_key or '',
        **details
    )
    
    if user and result != 'success':
        # Check for suspicious activity
        suspicious_indicators = monitor.detect_suspicious_activity(user)
        
        if suspicious_indicators:
            monitor.log_security_event(
                user=user,
                event_type='suspicious_activity',
                description=f"Suspicious login activity detected: {', '.join(suspicious_indicators)}",
                metadata={'indicators': suspicious_indicators}
            )
        
        # Check for account lockout
        should_lock, reason = monitor.check_account_lockout(user)
        if should_lock:
            user.is_active = False
            user.save()
            
            monitor.log_security_event(
                user=user,
                event_type='account_locked',
                description=f"Account automatically locked: {reason}",
                metadata={'reason': reason}
            )

CSRF and Security Headers

Enhanced security configuration:

# settings.py (Security enhancements)

# CSRF Protection
CSRF_COOKIE_SECURE = not DEBUG
CSRF_COOKIE_HTTPONLY = True
CSRF_COOKIE_SAMESITE = 'Strict'
CSRF_TRUSTED_ORIGINS = ['https://yourdomain.com']

# Session Security
SESSION_COOKIE_SECURE = not DEBUG
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Strict'
SESSION_COOKIE_AGE = 3600  # 1 hour
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
SESSION_SAVE_EVERY_REQUEST = True

# Security Headers
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_HSTS_SECONDS = 31536000 if not DEBUG else 0
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_SSL_REDIRECT = not DEBUG
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

# Content Security Policy
CSP_DEFAULT_SRC = ["'self'"]
CSP_SCRIPT_SRC = ["'self'", "'unsafe-inline'", "https://cdn.tailwindcss.com", "https://unpkg.com"]
CSP_STYLE_SRC = ["'self'", "'unsafe-inline'", "https://cdn.tailwindcss.com"]
CSP_FONT_SRC = ["'self'", "https://fonts.googleapis.com", "https://fonts.gstatic.com"]
CSP_IMG_SRC = ["'self'", "data:", "https:"]

# Additional Security Settings
X_FRAME_OPTIONS = 'DENY'
ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'yourdomain.com']

# Password Validation
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',
    },
]

# Logging Configuration for Security
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'verbose': {
            'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
            'style': '{',
        },
        'security': {
            'format': '[{asctime}] {levelname} - SECURITY EVENT - {message}',
            'style': '{',
        },
    },
    'handlers': {
        'file': {
            'level': 'INFO',
            'class': 'logging.handlers.RotatingFileHandler',
            'filename': 'logs/django.log',
            'maxBytes': 1024*1024*5,  # 5 MB
            'backupCount': 5,
            'formatter': 'verbose',
        },
        'security_file': {
            'level': 'WARNING',
            'class': 'logging.handlers.RotatingFileHandler',
            'filename': 'logs/security.log',
            'maxBytes': 1024*1024*5,  # 5 MB
            'backupCount': 10,
            'formatter': 'security',
        },
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
            'formatter': 'verbose',
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file', 'console'] if DEBUG else ['file'],
            'level': 'INFO',
            'propagate': True,
        },
        'security': {
            'handlers': ['security_file', 'console'] if DEBUG else ['security_file'],
            'level': 'WARNING',
            'propagate': True,
        },
        'accounts': {
            'handlers': ['file', 'console'] if DEBUG else ['file'],
            'level': 'INFO',
            'propagate': True,
        },
    },
}

Testing Your Django OTP System

Unit Tests

Create comprehensive unit tests:

# accounts/tests.py
from django.test import TestCase, Client
from django.contrib.auth.models import User
from django.urls import reverse
from django.utils import timezone
from unittest.mock import patch, MagicMock
import json

from .models import EmailOTP, UserProfile, LoginAttempt, SecurityEvent
from .utils import send_otp_email, validate_otp_rate_limit

class OTPAuthenticationTestCase(TestCase):
    """Test OTP authentication functionality"""
    
    def setUp(self):
        self.client = Client()
        self.user = User.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='testpassword123'
        )
        self.profile = UserProfile.objects.create(
            user=self.user,
            otp_enabled=True
        )

    def test_login_without_otp(self):
        """Test login when OTP is disabled"""
        self.profile.otp_enabled = False
        self.profile.save()
        
        response = self.client.post('/accounts/api/send-login-otp/', {
            'username_or_email': 'testuser',
            'password': 'testpassword123'
        })
        
        self.assertEqual(response.status_code, 200)
        data = json.loads(response.content)
        self.assertTrue(data['success'])
        self.assertFalse(data.get('otp_required', False))

    @patch('accounts.utils.send_mail')
    def test_login_with_otp(self, mock_send_mail):
        """Test login when OTP is enabled"""
        mock_send_mail.return_value = True
        
        response = self.client.post('/accounts/api/send-login-otp/', {
            'username_or_email': 'testuser',
            'password': 'testpassword123'
        })
        
        self.assertEqual(response.status_code, 200)
        data = json.loads(response.content)
        self.assertTrue(data['success'])
        self.assertTrue(data.get('otp_required', False))
        
        # Check if OTP was created
        otp = EmailOTP.objects.filter(user=self.user, purpose='login').first()
        self.assertIsNotNone(otp)
        self.assertTrue(otp.is_valid())

    def test_invalid_credentials(self):
        """Test login with invalid credentials"""
        response = self.client.post('/accounts/api/send-login-otp/', {
            'username_or_email': 'testuser',
            'password': 'wrongpassword'
        })
        
        self.assertEqual(response.status_code, 400)
        data = json.loads(response.content)
        self.assertFalse(data['success'])
        
        # Check if login attempt was logged
        attempt = LoginAttempt.objects.filter(username='testuser').first()
        self.assertIsNotNone(attempt)
        self.assertEqual(attempt.result, 'invalid_credentials')

    def test_otp_verification_success(self):
        """Test successful OTP verification"""
        # Create OTP
        otp = EmailOTP.generate_otp(
            user=self.user,
            purpose='login',
            ip_address='127.0.0.1'
        )
        
        # Set session data
        session = self.client.session
        session['otp_login_data'] = {
            'user_id': self.user.id,
            'username': 'testuser',
            'remember_me': False,
            'timestamp': timezone.now().isoformat(),
            'ip_address': '127.0.0.1'
        }
        session.save()
        
        response = self.client.post('/accounts/api/verify-login-otp/', {
            'otp_code': otp.otp_code
        })
        
        self.assertEqual(response.status_code, 200)
        data = json.loads(response.content)
        self.assertTrue(data['success'])
        
        # Check if user is logged in
        self.assertTrue('_auth_user_id' in self.client.session)

    def test_otp_verification_invalid_code(self):
        """Test OTP verification with invalid code"""
        # Create OTP
        otp = EmailOTP.generate_otp(
            user=self.user,
            purpose='login',
            ip_address='127.0.0.1'
        )
        
        # Set session data
        session = self.client.session
        session['otp_login_data'] = {
            'user_id': self.user.id,
            'username': 'testuser',
            'remember_me': False,
            'timestamp': timezone.now().isoformat(),
            'ip_address': '127.0.0.1'
        }
        session.save()
        
        response = self.client.post('/accounts/api/verify-login-otp/', {
            'otp_code': '123456'  # Wrong code
        })
        
        self.assertEqual(response.status_code, 400)
        data = json.loads(response.content)
        self.assertFalse(data['success'])
        
        # Check if attempt was incremented
        otp.refresh_from_db()
        self.assertEqual(otp.attempts, 1)

    def test_otp_expiration(self):
        """Test OTP expiration"""
        # Create expired OTP
        otp = EmailOTP.objects.create(
            user=self.user,
            otp_code='123456',
            purpose='login',
            expires_at=timezone.now() - timezone.timedelta(minutes=1),
            ip_address='127.0.0.1'
        )
        
        self.assertFalse(otp.is_valid())
        otp.refresh_from_db()
        self.assertEqual(otp.status, 'expired')

    def test_rate_limiting(self):
        """Test rate limiting functionality"""
        # Test user rate limiting
        self.profile.last_otp_request = timezone.now()
        self.profile.save()
        
        is_valid, message = validate_otp_rate_limit(self.user, '127.0.0.1')
        self.assertFalse(is_valid)

    def test_otp_enable_disable(self):
        """Test enabling and disabling OTP"""
        self.profile.otp_enabled = False
        self.profile.save()
        
        # Login user
        self.client.force_login(self.user)
        
        # Enable OTP
        with patch('accounts.utils.send_mail', return_value=True):
            response = self.client.post('/accounts/api/toggle-otp/', {
                'enable_otp': True
            })
            
            self.assertEqual(response.status_code, 200)
            data = json.loads(response.content)
            self.assertTrue(data['success'])
            self.assertTrue(data.get('verification_required', False))

class SecurityTestCase(TestCase):
    """Test security features"""
    
    def setUp(self):
        self.client = Client()
        self.user = User.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='testpassword123'
        )

    def test_login_attempt_logging(self):
        """Test that login attempts are properly logged"""
        initial_count = LoginAttempt.objects.count()
        
        self.client.post('/accounts/api/send-login-otp/', {
            'username_or_email': 'testuser',
            'password': 'wrongpassword'
        })
        
        self.assertEqual(LoginAttempt.objects.count(), initial_count + 1)
        
        attempt = LoginAttempt.objects.latest('timestamp')
        self.assertEqual(attempt.username, 'testuser')
        self.assertEqual(attempt.result, 'invalid_credentials')

    def test_security_event_logging(self):
        """Test security event logging"""
        from .security import SecurityMonitor
        
        request = self.client.request().wsgi_request
        monitor = SecurityMonitor(request)
        
        initial_count = SecurityEvent.objects.count()
        
        monitor.log_security_event(
            user=self.user,
            event_type='test_event',
            description='Test security event'
        )
        
        self.assertEqual(SecurityEvent.objects.count(), initial_count + 1)

class IntegrationTestCase(TestCase):
    """Integration tests for the complete authentication flow"""
    
    def setUp(self):
        self.client = Client()
        self.user = User.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='testpassword123'
        )
        UserProfile.objects.create(user=self.user, otp_enabled=True)

    @patch('accounts.utils.send_mail')
    def test_complete_login_flow(self, mock_send_mail):
        """Test complete login flow with OTP"""
        mock_send_mail.return_value = True
        
        # Step 1: Initial login request
        response = self.client.post('/accounts/api/send-login-otp/', 
            json.dumps({
                'username_or_email': 'testuser',
                'password': 'testpassword123',
                'remember_me': False
            }),
            content_type='application/json'
        )
        
        self.assertEqual(response.status_code, 200)
        data = json.loads(response.content)
        self.assertTrue(data['success'])
        self.assertTrue(data['otp_required'])
        
        # Step 2: Get the OTP from database
        otp = EmailOTP.objects.filter(user=self.user, purpose='login').first()
        self.assertIsNotNone(otp)
        
        # Step 3: Verify OTP
        response = self.client.post('/accounts/api/verify-login-otp/',
            json.dumps({
                'otp_code': otp.otp_code
            }),
            content_type='application/json'
        )
        
        self.assertEqual(response.status_code, 200)
        data = json.loads(response.content)
        self.assertTrue(data['success'])
        
        # Step 4: Check if user is logged in
        self.assertTrue('_auth_user_id' in self.client.session)
        
        # Step 5: Verify OTP is marked as used
        otp.refresh_from_db()
        self.assertEqual(otp.status, 'used')

    def test_login_page_rendering(self):
        """Test that login page renders correctly"""
        response = self.client.get('/accounts/login/')
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'Sign in to your account')
        self.assertContains(response, 'otp-input')

    def test_dashboard_access_control(self):
        """Test dashboard access control"""
        # Unauthenticated access should redirect
        response = self.client.get('/accounts/dashboard/')
        self.assertEqual(response.status_code, 302)  # Redirect to login
        
        # Authenticated access should work
        self.client.force_login(self.user)
        response = self.client.get('/accounts/dashboard/')
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'Dashboard')

def run_tests():
    """Run all tests"""
    from django.test.utils import get_runner
    from django.conf import settings
    
    TestRunner = get_runner(settings)
    test_runner = TestRunner()
    failures = test_runner.run_tests(['accounts'])
    
    return failures == 0

Manual Testing Checklist

Create a comprehensive testing checklist:

# Django OTP Authentication Testing Checklist

## Functional Tests

### Login Process
- [ ] Login page loads correctly
- [ ] Form validation works for empty fields
- [ ] Invalid credentials show appropriate error
- [ ] Valid credentials without OTP log in directly
- [ ] Valid credentials with OTP enabled show OTP form
- [ ] OTP email is sent successfully
- [ ] Email contains correct OTP code
- [ ] OTP form accepts 6-digit codes only
- [ ] Invalid OTP shows error message
- [ ] Valid OTP completes login
- [ ] Remember Me functionality works
- [ ] Session expires correctly

### OTP Management
- [ ] OTP can be enabled from dashboard
- [ ] Verification email sent when enabling
- [ ] OTP enabling requires email verification
- [ ] Backup codes generated when enabling
- [ ] OTP can be disabled with confirmation
- [ ] Security alert sent when toggling OTP

### Security Features
- [ ] Rate limiting prevents brute force attacks
- [ ] Failed attempts are logged
- [ ] Suspicious activity is detected
- [ ] Account lockout works after multiple failures
- [ ] IP addresses are tracked correctly
- [ ] Security events are logged

## UI/UX Tests

### Responsiveness
- [ ] Login form works on mobile devices
- [ ] OTP input fields are touch-friendly
- [ ] Dashboard is mobile-responsive
- [ ] All interactive elements work on touch screens

### Accessibility
- [ ] Screen readers can navigate forms
- [ ] Keyboard navigation works throughout
- [ ] Color contrast meets accessibility standards
- [ ] Error messages are announced properly

### Performance
- [ ] Page loads in under 2 seconds
- [ ] OTP email sends within 30 seconds
- [ ] Form submissions respond quickly
- [ ] No JavaScript errors in console

## Security Tests

### Input Validation
- [ ] SQL injection attempts blocked
- [ ] XSS attempts sanitized
- [ ] CSRF protection working
- [ ] File upload restrictions (if applicable)

### Session Management
- [ ] Sessions expire appropriately
- [ ] Session fixation prevented
- [ ] Logout clears session completely
- [ ] Concurrent session handling

### Email Security
- [ ] OTP codes are cryptographically secure
- [ ] Email content doesn't expose sensitive data
- [ ] Rate limiting prevents email bombing
- [ ] Expired OTPs cannot be used

## Integration Tests

### Email Provider
- [ ] Gmail SMTP works correctly
- [ ] SendGrid integration functional
- [ ] Email delivery rates acceptable
- [ ] Bounce handling configured

### Database
- [ ] All migrations run successfully
- [ ] Data integrity maintained
- [ ] Performance acceptable under load
- [ ] Backup and recovery tested

### External Services
- [ ] GeoIP lookup works (if enabled)
- [ ] CDN serves static files correctly
- [ ] Monitoring systems functional

 

As I have Faced this Issue and its Solved! I love to share it... Thanks 

Related Articles

Discussion

Have thoughts or questions about this article? Join the discussion!

Leave a Comment