Authentication System
The Django-CFG sample project demonstrates a secure OTP (One-Time Password) authentication system with user registration tracking. This guide covers the complete authentication flow and user management.
Authentication Overview
The sample project uses:
- OTP Authentication - Passwordless login via email/SMS
- JWT Tokens - Secure API authentication
- User Registration Tracking - Analytics for acquisition sources
- Custom User Model - Email-based authentication
OTP Authentication System
Authentication Flow
- User requests OTP (via email or SMS)
- System generates and sends OTP code
- User verifies OTP code
- System issues JWT tokens for API access
Requesting OTP
Send OTP to user’s email or phone:
from django_cfg.apps.accounts.services import OTPService
# Request OTP via email
success, error = OTPService.request_otp(
email="[email protected]",
source_url="https://myapp.com"
)
if success:
print("OTP sent to email")
else:
print(f"Failed to send OTP: {error}")Verifying OTP
Validate the OTP code and authenticate user:
from django_cfg.apps.accounts.services import OTPService
# Verify OTP
user = OTPService.verify_otp(
email="[email protected]",
otp_code="123456",
source_url="https://myapp.com"
)
if user:
print(f"User authenticated: {user.email}")
# Generate JWT tokens
from rest_framework_simplejwt.tokens import RefreshToken
refresh = RefreshToken.for_user(user)
access_token = str(refresh.access_token)
refresh_token = str(refresh)
else:
print("Invalid OTP code")OTP Configuration
Configure OTP settings:
# OTP expiration time (seconds)
OTP_EXPIRATION = 300 # 5 minutes
# OTP length
OTP_LENGTH = 6 # 6-digit code
# Max verification attempts
OTP_MAX_ATTEMPTS = 3API Authentication
Authentication Endpoints
POST /api/auth/otp/request/ # Request OTP
POST /api/auth/otp/verify/ # Verify OTP and get tokens
POST /api/auth/token/refresh/ # Refresh access token
POST /api/auth/logout/ # Logout userSee API Documentation for complete endpoint details.
Request OTP (API)
curl -X POST http://localhost:8000/api/auth/otp/request/ \
-H "Content-Type: application/json" \
-d '{
"identifier": "[email protected]",
"channel": "email"
}'Response:
{
"success": true,
"message": "OTP sent to email"
}Verify OTP (API)
curl -X POST http://localhost:8000/api/auth/otp/verify/ \
-H "Content-Type: application/json" \
-d '{
"identifier": "[email protected]",
"otp_code": "123456"
}'Response:
{
"success": true,
"tokens": {
"access": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGc..."
},
"user": {
"id": 1,
"email": "[email protected]",
"first_name": "John",
"last_name": "Doe"
}
}Using JWT Tokens
Include access token in API requests:
curl -X GET http://localhost:8000/api/profile/ \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc..."Refreshing Tokens
Access tokens expire after a set time. Refresh them:
curl -X POST http://localhost:8000/api/auth/token/refresh/ \
-H "Content-Type: application/json" \
-d '{
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGc..."
}'Response:
{
"access": "eyJ0eXAiOiJKV1QiLCJhbGc..."
}User Registration Tracking
Registration Source Tracking
Track where users come from for analytics:
from django_cfg.apps.accounts.models import RegistrationSource, UserRegistrationSource
# Automatic source creation during OTP verification
user, created = CustomUser.objects.register_user(
email="[email protected]",
source_url="https://dashboard.myapp.com"
)
if created:
# New user registered
source_link = user.userregistrationsource_set.first()
print(f"New user from: {source_link.source.get_display_name()}")Source URL Patterns
The system automatically categorizes sources:
# Dashboard registration
source_url = "https://dashboard.myapp.com"
# Category: "Dashboard"
# Marketing campaign
source_url = "https://myapp.com?utm_source=facebook&utm_campaign=spring"
# Category: "Facebook - Spring Campaign"
# Direct registration
source_url = "https://myapp.com/register"
# Category: "Website Registration"Analytics Queries
Query user acquisition data:
from django.db.models import Count
# Top acquisition sources
top_sources = RegistrationSource.objects.annotate(
user_count=Count('userregistrationsource')
).order_by('-user_count')[:10]
for source in top_sources:
print(f"{source.get_display_name()}: {source.user_count} users")
# Users by source this month
from django.utils import timezone
from datetime import timedelta
month_ago = timezone.now() - timedelta(days=30)
recent_sources = UserRegistrationSource.objects.filter(
created_at__gte=month_ago
).values('source__url').annotate(
count=Count('id')
).order_by('-count')
for source in recent_sources:
print(f"{source['source__url']}: {source['count']} users")Custom User Model
User Model
The sample project uses a custom user model:
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.db import models
class CustomUser(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(unique=True)
first_name = models.CharField(max_length=30, blank=True)
last_name = models.CharField(max_length=30, blank=True)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
date_joined = models.DateTimeField(auto_now_add=True)
last_login = models.DateTimeField(null=True, blank=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
def get_full_name(self):
return f"{self.first_name} {self.last_name}".strip()
def get_short_name(self):
return self.first_nameUser Manager
Custom manager for user creation:
from django.contrib.auth.models import BaseUserManager
class CustomUserManager(BaseUserManager):
def create_user(self, email, password=None, **extra_fields):
if not email:
raise ValueError('Email is required')
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
if password:
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password=None, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
return self.create_user(email, password, **extra_fields)
def register_user(self, email, source_url=None):
"""Register user with source tracking."""
user, created = self.get_or_create(email=email)
if created and source_url:
# Track registration source
source, _ = RegistrationSource.objects.get_or_create(
url=source_url
)
UserRegistrationSource.objects.create(
user=user,
source=source
)
return user, createdEmail Integration
Sending OTP via Email
The system uses SendGrid (production) or console (development):
from django_cfg import DjangoEmailService
email_service = DjangoEmailService()
# Send OTP email
def send_otp_email(user_email, otp_code):
email_service.send_template(
template_name="emails/otp.html",
context={
"otp_code": otp_code,
"expiry_minutes": 5
},
recipient_list=[user_email],
subject="Your verification code"
)Email Template
<!-- templates/emails/otp.html -->
<!DOCTYPE html>
<html>
<head>
<style>
.otp-code {
font-size: 32px;
font-weight: bold;
letter-spacing: 5px;
color: #4F46E5;
}
</style>
</head>
<body>
<h2>Your Verification Code</h2>
<p>Enter this code to complete your login:</p>
<div class="otp-code">{{ otp_code }}</div>
<p>This code expires in {{ expiry_minutes }} minutes.</p>
<p>If you didn't request this code, please ignore this email.</p>
</body>
</html>See Service Integrations for email configuration.
SMS Integration
Sending OTP via SMS
Use Twilio for SMS delivery:
from django_cfg.modules.django_twilio.service import UnifiedOTPService
twilio_service = UnifiedOTPService()
# Send OTP via SMS
def send_otp_sms(phone_number, otp_code):
message = twilio_service.send_sms(
to=phone_number,
body=f"Your verification code is: {otp_code}"
)
return message.sidSee Service Integrations for Twilio setup.
Security Best Practices
1. Rate Limiting
Prevent brute force attacks:
from django.core.cache import cache
def check_rate_limit(identifier):
"""Check if user exceeded OTP request rate."""
cache_key = f"otp_rate_limit:{identifier}"
attempts = cache.get(cache_key, 0)
if attempts >= 3: # Max 3 requests per hour
return False
cache.set(cache_key, attempts + 1, 3600) # 1 hour
return True2. OTP Expiration
OTP codes expire after 5 minutes:
from django.utils import timezone
from datetime import timedelta
# Check if OTP is expired
def is_otp_valid(otp_created_at):
expiry_time = timedelta(minutes=5)
return timezone.now() - otp_created_at < expiry_time3. Single Use OTP
OTP codes can only be used once:
def verify_and_consume_otp(email, otp_code):
"""Verify OTP and mark as used."""
otp = OTPSecret.objects.filter(
email=email,
secret=otp_code,
used=False
).first()
if otp and is_otp_valid(otp.created_at):
otp.used = True
otp.save()
return True
return False4. Secure Token Storage
Store tokens securely in client applications:
# ✅ Good: Secure storage
# - HTTP-only cookies (web)
# - Secure keychain (mobile)
# - Encrypted storage
# ❌ Bad: Insecure storage
# - localStorage (XSS vulnerable)
# - Plain text files
# - URL parametersUser Profile Management
Profile Model
Extend user with profile information:
# apps/profiles/models.py
from django.db import models
from django.contrib.auth import get_user_model
User = get_user_model()
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
bio = models.TextField(blank=True)
avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)
phone = models.CharField(max_length=20, blank=True)
date_of_birth = models.DateField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.user.email}'s profile"Auto-create Profile
Automatically create profile for new users:
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
instance.profile.save()Testing Authentication
Test OTP Flow
from django.test import TestCase
from django_cfg.apps.accounts.services import OTPService
class AuthenticationTest(TestCase):
def test_otp_request(self):
"""Test OTP request."""
success, error = OTPService.request_otp(
email="[email protected]",
source_url="https://test.com"
)
self.assertTrue(success)
def test_otp_verification(self):
"""Test OTP verification."""
# Request OTP
OTPService.request_otp(
email="[email protected]",
source_url="https://test.com"
)
# Get OTP from database
from django_cfg.apps.accounts.models import OTPSecret
otp = OTPSecret.objects.get(email="[email protected]")
# Verify OTP
user = OTPService.verify_otp(
email="[email protected]",
otp_code=otp.secret,
source_url="https://test.com"
)
self.assertIsNotNone(user)
self.assertEqual(user.email, "[email protected]")Best Practices
1. Use OTP for Passwordless Auth
# ✅ Good: Passwordless OTP
user = OTPService.verify_otp(email, otp_code, source_url)
# ❌ Bad: Password-based auth (less secure)
user = authenticate(username=email, password=password)2. Track User Sources
# ✅ Good: Track registration source
user, created = User.objects.register_user(
email=email,
source_url=request.META.get('HTTP_REFERER')
)
# ❌ Bad: No source tracking
user = User.objects.create(email=email)3. Implement Rate Limiting
# ✅ Good: Rate limit OTP requests
if not check_rate_limit(email):
return {"error": "Too many requests"}
# ❌ Bad: No rate limiting4. Set Token Expiration
# ✅ Good: Short-lived access tokens
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
}
# ❌ Bad: Long-lived tokens
ACCESS_TOKEN_LIFETIME = timedelta(days=365)Related Topics
- API Documentation - Authentication endpoints
- Service Integrations - Email and SMS setup
- Configuration - Authentication configuration
Secure, passwordless authentication improves user experience and security!