Django User Signals
Introduction
Django's authentication system provides a set of built-in signals related to user actions, such as when users are created, updated, login, or logout. These signals allow you to execute custom code in response to these events without modifying Django's core user management functionality.
In this tutorial, we'll explore the different user signals available in Django, how to connect functions to these signals, and practical examples of how they can be used in real-world applications.
User Signals in Django
Django's authentication framework (django.contrib.auth
) provides several signals that are dispatched during user-related events:
user_logged_in
- Sent when a user logs in successfullyuser_logged_out
- Sent when a user logs out successfullyuser_login_failed
- Sent when a user login failspre_save
andpost_save
- For the User model (create/update operations)pre_delete
andpost_delete
- For the User model (delete operations)
Let's explore each of these signals with examples.
Setting Up Signal Receivers
First, let's set up a Django project structure for handling user signals. It's considered a best practice to create a dedicated file called signals.py
within your app:
# myapp/signals.py
from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed
from django.contrib.auth.models import User
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete
from django.dispatch import receiver
# We'll add our signal handlers here
To ensure that our signals are loaded when Django starts, we need to import them in the app's apps.py
file:
# myapp/apps.py
from django.apps import AppConfig
class MyappConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'myapp'
def ready(self):
import myapp.signals # Import signals when the app is ready
Now, let's explore each type of user signal with examples.
Login and Logout Signals
User Login Signal
The user_logged_in
signal is dispatched when a user successfully logs in. Let's create a handler to log when users login:
@receiver(user_logged_in)
def user_logged_in_handler(sender, request, user, **kwargs):
"""
Handle user login events
"""
# Log the login
ip_address = get_client_ip(request)
# Create a login record in our own custom model
UserLoginActivity.objects.create(
user=user,
ip_address=ip_address,
user_agent=request.META.get('HTTP_USER_AGENT', '')
)
print(f"User {user.username} logged in from IP: {ip_address}")
def get_client_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
Here's how we might define a model to track login activity:
# myapp/models.py
from django.db import models
from django.conf import settings
class UserLoginActivity(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
login_datetime = models.DateTimeField(auto_now_add=True)
ip_address = models.GenericIPAddressField(null=True)
user_agent = models.CharField(max_length=255)
def __str__(self):
return f"{self.user.username} - {self.login_datetime}"
User Logout Signal
Similarly, we can track when users log out:
@receiver(user_logged_out)
def user_logged_out_handler(sender, request, user, **kwargs):
"""
Handle user logout events
"""
if user: # User might be None if session is expired
print(f"User {user.username} logged out")
# Update the last logout time in user profile
if hasattr(user, 'profile'):
user.profile.last_logout = timezone.now()
user.profile.save(update_fields=['last_logout'])
Login Failed Signal
The user_login_failed
signal is sent when a login attempt fails. This can be useful for security monitoring:
@receiver(user_login_failed)
def user_login_failed_handler(sender, credentials, request, **kwargs):
"""
Handle failed login attempts
"""
username = credentials.get('username', '')
ip_address = get_client_ip(request)
print(f"Failed login attempt for username: {username} from IP: {ip_address}")
# Log failed attempts to detect brute force attacks
FailedLoginAttempt.objects.create(
username=username,
ip_address=ip_address,
user_agent=request.META.get('HTTP_USER_AGENT', '')
)
# Check for multiple failed attempts from same IP
recent_failures = FailedLoginAttempt.objects.filter(
ip_address=ip_address,
timestamp__gte=timezone.now() - timedelta(hours=1)
).count()
if recent_failures > 5:
# Consider implementing security measures like
# temporary IP bans or CAPTCHA requirements
print(f"WARNING: Multiple failed login attempts from {ip_address}")
User Creation and Update Signals
Django's pre_save
and post_save
signals can be used with the User model to react to user creation or updates.
User Creation Signal
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
"""
Create a user profile when a new user is created
"""
if created:
print(f"New user created: {instance.username}")
# Create an associated profile
UserProfile.objects.create(
user=instance,
email_verified=False
)
# Send welcome email
send_welcome_email(instance)
def send_welcome_email(user):
"""Send a welcome email to the newly registered user"""
subject = 'Welcome to our website!'
message = f'Hi {user.username}, thank you for registering!'
# Email sending logic here
print(f"Welcome email sent to {user.email}")
User Update Signal
@receiver(pre_save, sender=User)
def user_update_handler(sender, instance, **kwargs):
"""
Track changes to user objects
"""
if instance.pk: # If user already exists (not new)
try:
old_user = User.objects.get(pk=instance.pk)
# Check if email was changed
if old_user.email != instance.email:
print(f"User {instance.username} changed email: {old_user.email} -> {instance.email}")
# If using email verification, you might want to:
if hasattr(instance, 'profile'):
instance.profile.email_verified = False
instance.profile.save(update_fields=['email_verified'])
except User.DoesNotExist:
pass # This should not happen generally
User Deletion Signal
When a user is deleted, you might want to perform cleanup operations:
@receiver(pre_delete, sender=User)
def user_pre_delete_handler(sender, instance, **kwargs):
"""
Actions to take before a user is deleted
"""
print(f"About to delete user: {instance.username}")
# Archive user data if needed
UserArchive.objects.create(
username=instance.username,
email=instance.email,
date_joined=instance.date_joined,
last_login=instance.last_login,
archive_date=timezone.now()
)
@receiver(post_delete, sender=User)
def user_post_delete_handler(sender, instance, **kwargs):
"""
Actions to take after a user is deleted
"""
print(f"User {instance.username} has been deleted")
# Cleanup any orphaned data if needed
Real-World Applications
Now that we've seen the basic patterns, let's look at some practical real-world applications of user signals.
1. Security Audit Trail
# models.py
class UserSecurityAudit(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True)
username = models.CharField(max_length=150) # Store even if user is deleted
action = models.CharField(max_length=50) # login, logout, failed_login, etc.
ip_address = models.GenericIPAddressField(null=True)
timestamp = models.DateTimeField(auto_now_add=True)
details = models.JSONField(default=dict)
def __str__(self):
return f"{self.username} - {self.action} - {self.timestamp}"
# signals.py
def log_security_event(username, action, request=None, details=None):
"""Helper function to log security events"""
ip = None
if request:
ip = get_client_ip(request)
UserSecurityAudit.objects.create(
username=username,
action=action,
ip_address=ip,
details=details or {}
)
@receiver(user_logged_in)
def audit_user_login(sender, request, user, **kwargs):
log_security_event(
user.username,
'login',
request,
{'user_agent': request.META.get('HTTP_USER_AGENT', '')}
)
@receiver(user_logged_out)
def audit_user_logout(sender, request, user, **kwargs):
if user: # User can be None if session expired
log_security_event(user.username, 'logout', request)
@receiver(user_login_failed)
def audit_failed_login(sender, credentials, request, **kwargs):
log_security_event(
credentials.get('username', 'unknown'),
'failed_login',
request,
{'attempt_count': track_failed_attempts(request)}
)
2. Profile Enrichment
@receiver(post_save, sender=User)
def enrich_user_profile(sender, instance, created, **kwargs):
"""
Enhance user profiles with additional data when users are created.
This can include default preferences, language settings based on IP, etc.
"""
if created and hasattr(instance, 'profile'):
# Set default preferences
instance.profile.notification_preferences = {
'email_news': True,
'email_updates': True,
'sms_alerts': False
}
# Set timezone based on registration info if available
if hasattr(instance, '_signup_request'):
request = instance._signup_request
# Use a geolocation service to get timezone from IP
# (Pseudocode - implementation would depend on your geo service)
timezone_name = get_timezone_from_ip(get_client_ip(request))
if timezone_name:
instance.profile.timezone = timezone_name
instance.profile.save()
3. Multi-factor Authentication Enhancement
@receiver(user_logged_in)
def check_mfa_requirements(sender, request, user, **kwargs):
"""
After login, check if the user needs to complete MFA
"""
# Check if MFA is enabled for the user
if hasattr(user, 'mfa_settings') and user.mfa_settings.is_enabled:
# Mark the session as needing MFA verification
request.session['mfa_required'] = True
request.session['partial_login_user_id'] = user.id
# Store the originally requested URL to redirect after MFA
if 'next' in request.GET:
request.session['post_mfa_redirect'] = request.GET['next']
# Log that MFA is required
print(f"MFA required for user: {user.username}")
Summary
Django's user signals provide a powerful way to extend the built-in authentication system without modifying its core functionality. We've explored how to:
- Handle login, logout, and failed login events
- Respond to user creation, updates, and deletions
- Implement real-world examples including security auditing, profile enrichment, and multi-factor authentication
By leveraging these signals, you can create more robust user management, improve security, and provide a better user experience in your Django applications.
Additional Resources and Exercises
Resources
Exercises
-
Last Seen Tracker: Create a system that updates a
last_seen
field on user profiles whenever they log in or perform an action. -
Login History Dashboard: Build a simple admin interface or user dashboard that displays login history using the data collected from user login signals.
-
IP-Based Security: Extend the failed login handler to temporarily block IPs that have too many failed login attempts.
-
Custom Welcome Journey: Create a system that uses the user creation signal to start a custom onboarding email sequence for new users.
-
Password Expiry System: Use signals to check if a user's password is expired on login and redirect them to a password change page if needed.
Remember, while signals provide a convenient way to add functionality, be cautious about placing critical business logic in signal handlers as they can make the flow of your application harder to follow. Use them wisely for cross-cutting concerns that truly belong outside your main application logic.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)