Customization
This guide covers how to customize DRF Auth Kit to fit your specific needs. To effectively customize the system, you first need to understand the underlying authentication flow architecture.
Understanding the Authentication Flow
DRF Auth Kit uses a sophisticated serializer composition pattern that separates credential validation from token generation, enabling maximum flexibility and reusability.
Core Architecture: Request-Response Serializer Pattern
The authentication system is built on three types of serializers:
- Request Serializers (Input Validation)
Handle credential validation and user authentication:
LoginRequestSerializer- Validates username/password credentialsSocialLoginRequestSerializer- Validates OAuth tokens/codesKey Method:
validate()- authenticates user and setscontext["user"]
- Response Serializers (Output Generation)
Generate authentication tokens from authenticated users:
JWTResponseSerializer- Generates JWT access/refresh tokensTokenResponseSerializer- Generates DRF authentication tokensKey Method:
validate()- usescontext["user"]to create tokens
- Final Serializers (Request + Response Combined)
Inherit from both to create complete authentication flows:
class LoginSerializer(
JWTResponseSerializer, # First parent - token generation
LoginRequestSerializer # Second parent - credential validation
):
pass
The Magic: Method Resolution Order Chain
The authentication flow works through Python’s Method Resolution Order (MRO):
# When LoginSerializer.validate() is called:
# 1. JWTResponseSerializer.validate() calls super().validate()
# 2. LoginRequestSerializer.validate() authenticates user
# 3. JWTResponseSerializer.validate() generates JWT tokens
# 4. Returns: {"user": user_obj, "access": "jwt_token", "refresh": "jwt_token"}
Flow Steps:
Credential Validation: Request serializer authenticates user and stores in
context["user"]Token Generation: Response serializer uses authenticated user to generate tokens
Combined Output: Final result contains both user data and authentication tokens
MFA Flow: Adding Intermediate Steps
Multi-Factor Authentication extends the basic flow with ephemeral tokens:
Step 1 - Credential Validation + MFA Check:
class MFAFirstStepSerializer(
MFAFirstStepResponseSerializer, # Generates ephemeral token OR full auth
LoginRequestSerializer # Validates credentials
):
pass
Input: Username/password
Output: Ephemeral token (if MFA required) OR full authentication (if MFA disabled)
Step 2 - MFA Verification + Token Generation:
class MFASecondStepSerializer(
LoginResponseSerializer, # Generates final auth tokens
MFASecondStepRequestSerializer # Validates ephemeral token + MFA code
):
pass
Input: Ephemeral token + MFA verification code
Output: Full authentication tokens (same as standard login)
- Ephemeral Token System:
Temporary, short-lived tokens that prove first-step authentication completed, carrying user identity and MFA method information securely between steps.
Dynamic Serializer Factory Pattern
The system uses factory functions for runtime composition based on configuration:
def get_login_serializer():
# Choose response serializer based on AUTH_TYPE setting
if auth_kit_settings.AUTH_TYPE == 'jwt':
response_serializer = JWTResponseSerializer
elif auth_kit_settings.AUTH_TYPE == 'token':
response_serializer = TokenResponseSerializer
else:
response_serializer = CustomResponseSerializer
# Compose final serializer
return type('LoginSerializer', (response_serializer, LoginRequestSerializer), {})
This enables configuration-driven composition where settings determine which serializers are combined.
Customization Philosophy
Understanding this architecture enables powerful customization strategies:
- Modularity
Replace specific components without affecting others:
Custom request serializers for different authentication backends
Custom response serializers for different token formats
Custom composition for entirely new authentication flows
- Extensibility
Add new functionality by extending existing patterns:
Additional validation steps in request serializers
Extra fields in response serializers
Multi-step flows like MFA
- Reusability
Share common logic across different authentication methods:
Same token generation for multiple input methods
Same validation logic for multiple token types
Mix and match any request with any response serializer
Customization Examples
Now that you understand the architecture, here are practical examples of how to customize different parts of the system.
Custom Request Serializers (Input Validation)
Example 1: Custom Credential Validation
Add business logic to credential validation:
# serializers.py
from auth_kit.serializers import LoginRequestSerializer
from rest_framework import serializers
from django.contrib.auth import authenticate
from myapp.models import UserProfile
class CustomLoginRequestSerializer(LoginRequestSerializer):
company_code = serializers.CharField(required=True)
def validate(self, attrs):
# Custom business logic before authentication
company_code = attrs.get('company_code')
if not UserProfile.objects.filter(company_code=company_code).exists():
raise serializers.ValidationError('Invalid company code')
# Call parent validation to authenticate user
attrs = super().validate(attrs)
# Additional checks after authentication
user = self.context['user']
if not user.profile.company_code == company_code:
raise serializers.ValidationError('User not associated with this company')
return attrs
Example 2: API Key Authentication
Replace password authentication with API key:
from auth_kit.serializers import BaseLoginRequestSerializer
from django.contrib.auth import get_user_model
from rest_framework import serializers
User = get_user_model()
class APIKeyRequestSerializer(BaseLoginRequestSerializer):
api_key = serializers.CharField()
def validate(self, attrs):
api_key = attrs.get('api_key')
try:
user = User.objects.get(profile__api_key=api_key, is_active=True)
self.context['user'] = user # Set authenticated user in context
return attrs
except User.DoesNotExist:
raise serializers.ValidationError('Invalid API key')
Custom Response Serializers (Output Generation)
Example 1: Custom Token Format
Add custom fields to authentication response:
from auth_kit.serializers import JWTResponseSerializer
from rest_framework import serializers
class CustomJWTResponseSerializer(JWTResponseSerializer):
permissions = serializers.SerializerMethodField()
company_info = serializers.SerializerMethodField()
def get_permissions(self, obj):
user = self.context['user']
return list(user.get_all_permissions())
def get_company_info(self, obj):
user = self.context['user']
return {
'company_name': user.profile.company.name,
'company_code': user.profile.company_code,
}
Example 2: Custom Token Backend
Create entirely custom token system:
from auth_kit.serializers import BaseLoginResponseSerializer
from rest_framework import serializers
import jwt
from datetime import datetime, timedelta
class CustomTokenResponseSerializer(BaseLoginResponseSerializer):
custom_token = serializers.SerializerMethodField()
expires_at = serializers.SerializerMethodField()
def get_custom_token(self, obj):
user = self.context['user']
payload = {
'user_id': user.id,
'username': user.username,
'company_code': user.profile.company_code,
'exp': datetime.utcnow() + timedelta(hours=24)
}
return jwt.encode(payload, 'your-secret-key', algorithm='HS256')
def get_expires_at(self, obj):
return datetime.utcnow() + timedelta(hours=24)
Custom Final Serializers (Complete Flows)
Example 1: Combine Custom Request + Response
class CustomLoginSerializer(
CustomJWTResponseSerializer, # Custom token generation
CustomLoginRequestSerializer # Custom credential validation
):
"""
Complete custom login flow with company validation and enhanced tokens.
"""
pass
Example 2: Multi-Step Custom Flow
Create a flow similar to MFA but for different purposes:
class TwoFactorEmailSerializer(
EmailVerificationResponseSerializer, # Generates verification token
LoginRequestSerializer # Standard credential validation
):
"""Step 1: Validate credentials, send email verification."""
pass
class TwoFactorEmailVerifySerializer(
CustomJWTResponseSerializer, # Final token generation
EmailVerificationRequestSerializer # Validates email verification code
):
"""Step 2: Validate email code, generate final tokens."""
pass
Configuration Integration
Register Custom Serializers
Use settings to integrate your custom serializers:
# settings.py
AUTH_KIT = {
'AUTH_TYPE': 'custom',
# Custom serializer classes
'LOGIN_REQUEST_SERIALIZER': 'myapp.serializers.CustomLoginRequestSerializer',
'LOGIN_RESPONSE_SERIALIZER': 'myapp.serializers.CustomJWTResponseSerializer',
# Or override the complete serializer
'LOGIN_SERIALIZER': 'myapp.serializers.CustomLoginSerializer',
}
Factory Function Override
For more complex customization, override the factory functions:
# app_settings.py or serializers.py
from auth_kit.app_settings import auth_kit_settings
def custom_login_serializer_factory():
"""Custom factory that chooses serializers based on request or user type."""
# You can inspect request context here
if some_condition:
return CustomLoginSerializer
else:
return StandardLoginSerializer
# Override in settings
AUTH_KIT = {
'LOGIN_SERIALIZER_FACTORY': 'myapp.serializers.custom_login_serializer_factory',
}
Additional Customization Examples
Registration Serializer
Registration doesn’t use the request-response pattern, so it’s customized directly:
# serializers.py
from auth_kit.serializers import RegisterSerializer
from rest_framework import serializers
from django.contrib.auth import get_user_model
User = get_user_model()
class CustomRegisterSerializer(RegisterSerializer):
first_name = serializers.CharField(required=True, max_length=30)
last_name = serializers.CharField(required=True, max_length=30)
phone_number = serializers.CharField(required=False, max_length=20)
def validate_phone_number(self, value):
if value and not value.startswith('+'):
raise serializers.ValidationError("Phone number must start with '+'")
return value
def save(self, request):
user = super().save(request)
user.first_name = self.validated_data.get('first_name')
user.last_name = self.validated_data.get('last_name')
user.save()
# Create user profile with additional data
from .models import UserProfile
UserProfile.objects.create(
user=user,
phone_number=self.validated_data.get('phone_number', '')
)
return user
Configure it in settings:
AUTH_KIT = {
'REGISTER_SERIALIZER': 'myapp.serializers.CustomRegisterSerializer',
}
User Detail Serializer
Customize user profile serialization:
# serializers.py
from auth_kit.serializers import UserSerializer
from rest_framework import serializers
from django.contrib.auth import get_user_model
User = get_user_model()
class CustomUserSerializer(UserSerializer):
full_name = serializers.SerializerMethodField()
avatar = serializers.SerializerMethodField()
phone_number = serializers.CharField(source='profile.phone_number', read_only=True)
is_premium = serializers.BooleanField(source='profile.is_premium', read_only=True)
class Meta(UserSerializer.Meta):
fields = UserSerializer.Meta.fields + [
'full_name', 'avatar', 'phone_number', 'is_premium'
]
def get_full_name(self, obj):
return f"{obj.first_name} {obj.last_name}".strip()
def get_avatar(self, obj):
if hasattr(obj, 'profile') and obj.profile.avatar:
request = self.context.get('request')
if request:
return request.build_absolute_uri(obj.profile.avatar.url)
return None
def update(self, instance, validated_data):
# Handle nested profile updates
profile_data = validated_data.pop('profile', {})
# Update user fields
instance = super().update(instance, validated_data)
# Update profile fields
if profile_data:
profile = instance.profile
for attr, value in profile_data.items():
setattr(profile, attr, value)
profile.save()
return instance
Configure it in settings:
AUTH_KIT = {
'USER_SERIALIZER': 'myapp.serializers.CustomUserSerializer',
}
Custom Views
Custom Registration View
Override the registration view for custom logic:
# views.py
from auth_kit.views import RegisterView
from rest_framework.response import Response
from rest_framework import status
from django.core.mail import send_mail
from django.conf import settings
class CustomRegisterView(RegisterView):
def create(self, request, *args, **kwargs):
# Call parent create method
response = super().create(request, *args, **kwargs)
# Custom logic after user creation
if response.status_code == status.HTTP_201_CREATED:
user = response.data.get('user')
# Send welcome email
send_mail(
subject='Welcome to Our Platform!',
message=f'Welcome {user["first_name"]}! Your account has been created successfully.',
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[user['email']],
fail_silently=True
)
# Log user registration
import logging
logger = logging.getLogger('auth_kit')
logger.info(f'New user registered: {user["email"]}')
return response
def get_response_data(self, user):
# Customize response data
data = super().get_response_data(user)
data['welcome_message'] = 'Welcome to our platform!'
return data
Configure it in settings:
AUTH_KIT = {
'REGISTER_VIEW': 'myapp.views.CustomRegisterView',
}
Custom Login View
Override the login view for custom authentication logic:
# views.py
from auth_kit.views import LoginView
from rest_framework.response import Response
from rest_framework import status
from django.utils import timezone
from django.contrib.auth import login
class CustomLoginView(LoginView):
def login(self):
# Call parent login method
response = super().login()
# Custom logic after login
if hasattr(self, 'user'):
# Update last login time
self.user.last_login = timezone.now()
self.user.save()
# Log user activity
from .models import UserActivity
UserActivity.objects.create(
user=self.user,
action='login',
ip_address=self.get_client_ip(),
user_agent=self.request.META.get('HTTP_USER_AGENT', '')
)
return response
def get_client_ip(self):
x_forwarded_for = self.request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = self.request.META.get('REMOTE_ADDR')
return ip
def get_response_data(self, user):
# Customize response data
data = super().get_response_data(user)
data['last_login'] = user.last_login.isoformat() if user.last_login else None
data['login_count'] = user.activity_set.filter(action='login').count()
return data
Configure it in settings:
AUTH_KIT = {
'LOGIN_VIEW': 'myapp.views.CustomLoginView',
}
Custom Password Reset View
Override password reset functionality:
# views.py
from auth_kit.views import PasswordResetView
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.conf import settings
class CustomPasswordResetView(PasswordResetView):
def send_reset_email(self, user, reset_url):
# Custom email template and logic
context = {
'user': user,
'reset_url': reset_url,
'site_name': 'Your Site Name',
'support_email': settings.DEFAULT_FROM_EMAIL,
}
html_message = render_to_string('auth/password_reset_email.html', context)
text_message = render_to_string('auth/password_reset_email.txt', context)
send_mail(
subject='Password Reset Request',
message=text_message,
html_message=html_message,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[user.email],
fail_silently=False
)
# Log password reset request
import logging
logger = logging.getLogger('auth_kit')
logger.info(f'Password reset requested for user: {user.email}')
Configure it in settings:
AUTH_KIT = {
'PASSWORD_RESET_VIEW': 'myapp.views.CustomPasswordResetView',
}
Custom Email Templates
Email Template Customization
Since DRF Auth Kit is API-based, the main template customization you’ll need is for authentication emails. DRF Auth Kit uses Django Allauth’s email templates.
Available Email Templates
Create these templates in your Django project to customize emails:
templates/
└── account/
└── email/
├── email_confirmation_subject.txt # Email verification subject
├── email_confirmation_message.txt # Email verification body
├── password_reset_key_subject.txt # Password reset subject
└── password_reset_key_message.txt # Password reset body
Simple Example
<!-- templates/account/email/email_confirmation_message.txt -->
Hello {{ user.first_name }},
Please confirm your email address by clicking this link:
{{ activate_url }}
Thanks,
The {{ site_name }} Team
Template Configuration
# settings.py
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
# ... other template settings
},
]
External Resources
For comprehensive email template customization:
Django Allauth Email Templates - Complete template reference
Email Template Examples - Default templates on GitHub
Django Email Documentation - Email configuration guide
Template Variables
Common variables available in email templates:
{{ user }}- User object{{ site_name }}- Site name{{ activate_url }}- Email confirmation URL{{ password_reset_url }}- Password reset URL
Advanced Customization
Custom User Model
Use a custom user model with DRF Auth Kit:
# models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
class CustomUser(AbstractUser):
email = models.EmailField(unique=True)
phone_number = models.CharField(max_length=20, blank=True)
date_of_birth = models.DateField(null=True, blank=True)
is_premium = models.BooleanField(default=False)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
def __str__(self):
return self.email
Configure it in settings:
# settings.py
AUTH_USER_MODEL = 'myapp.CustomUser'
Custom Permissions
Add custom permissions to authentication views:
# permissions.py
from rest_framework.permissions import BasePermission
class IsPremiumUser(BasePermission):
def has_permission(self, request, view):
return request.user.is_authenticated and request.user.is_premium
class IsOwnerOrReadOnly(BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in ['GET', 'HEAD', 'OPTIONS']:
return True
return obj.user == request.user
Use custom permissions in views:
# views.py
from auth_kit.views import UserView
from .permissions import IsPremiumUser
class CustomUserView(UserView):
permission_classes = [IsPremiumUser]
def get_queryset(self):
return super().get_queryset().filter(is_premium=True)
Custom Middleware
Add custom middleware for authentication:
# middleware.py
from django.utils.deprecation import MiddlewareMixin
import logging
class AuthenticationLoggingMiddleware(MiddlewareMixin):
def process_request(self, request):
if request.path.startswith('/api/auth/'):
logger = logging.getLogger('auth_kit')
logger.info(f'Auth request: {request.method} {request.path} from {request.META.get("REMOTE_ADDR")}')
def process_response(self, request, response):
if request.path.startswith('/api/auth/'):
logger = logging.getLogger('auth_kit')
logger.info(f'Auth response: {response.status_code} for {request.path}')
return response
Configure it in settings:
# settings.py
MIDDLEWARE = [
# ... other middleware
'myapp.middleware.AuthenticationLoggingMiddleware',
]
Testing Custom Components
Testing Custom Serializers
from django.test import TestCase
from rest_framework.test import APIRequestFactory
from myapp.serializers import CustomRegisterSerializer
class CustomSerializerTestCase(TestCase):
def setUp(self):
self.factory = APIRequestFactory()
def test_custom_register_serializer(self):
data = {
'username': 'testuser',
'email': 'test@example.com',
'password1': 'testpass123',
'password2': 'testpass123',
'first_name': 'Test',
'last_name': 'User',
'phone_number': '+1234567890'
}
request = self.factory.post('/api/auth/registration/', data)
serializer = CustomRegisterSerializer(data=data)
serializer.context['request'] = request
self.assertTrue(serializer.is_valid())
user = serializer.save(request)
self.assertEqual(user.first_name, 'Test')
self.assertEqual(user.last_name, 'User')
self.assertTrue(hasattr(user, 'profile'))
self.assertEqual(user.profile.phone_number, '+1234567890')
Testing Custom Views
from django.test import TestCase
from rest_framework.test import APIClient
from django.contrib.auth import get_user_model
User = get_user_model()
class CustomViewTestCase(TestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpass123'
)
def test_custom_login_view(self):
response = self.client.post('/api/auth/login/', {
'username': 'testuser',
'password': 'testpass123'
})
self.assertEqual(response.status_code, 200)
self.assertIn('access_token', response.data)
self.assertIn('login_count', response.data)
# Check that user activity was logged
from myapp.models import UserActivity
activity = UserActivity.objects.filter(
user=self.user,
action='login'
).first()
self.assertIsNotNone(activity)
Best Practices
Type Hints: Always include type hints in custom components
Documentation: Document custom components for OpenAPI schema
Testing: Write tests for all custom components
Backwards Compatibility: Ensure customizations don’t break existing functionality
Security: Follow security best practices in custom authentication logic
Performance: Consider performance impact of custom components
Logging: Add appropriate logging for debugging and monitoring
Next Steps
Now that you understand DRF Auth Kit’s architecture and customization capabilities:
- Understanding the Architecture
The key to effective customization is understanding the request-response serializer composition pattern and how the Method Resolution Order creates the authentication flow.
- Start Simple
Begin with simple customizations like adding fields to response serializers or custom validation in request serializers before attempting complex multi-step flows.
- Advanced Customization
For complex requirements, you can create entirely new authentication flows by composing custom request and response serializers, following the same patterns used by MFA and social authentication.
Further Learning
Source Code: Review the DRF Auth Kit source code to see more examples of the composition pattern
MFA Implementation: Study the MFA module to understand multi-step authentication flows
Social Authentication: Examine the social authentication module to see how it reuses response serializers
Contribution: Consider contributing useful custom components back to the project
- Key Takeaway
The serializer composition architecture enables you to customize any part of the authentication flow while maintaining compatibility and reusing existing components. Understanding this pattern unlocks the full power of DRF Auth Kit’s flexibility.
Social Authentication: Reusing Response Logic
Social authentication demonstrates the power of the composition pattern:
Key Benefits:
Reuses Response Logic: Same token generation across all authentication methods
Custom Request Logic: OAuth token/code validation instead of password validation
Consistent Output: Social login returns identical token structure as regular login