Skip to main content

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:

  1. user_logged_in - Sent when a user logs in successfully
  2. user_logged_out - Sent when a user logs out successfully
  3. user_login_failed - Sent when a user login fails
  4. pre_save and post_save - For the User model (create/update operations)
  5. pre_delete and post_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:

python
# 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:

python
# 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:

python
@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:

python
# 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:

python
@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:

python
@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

python
@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

python
@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:

python
@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

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

python
@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

python
@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:

  1. Handle login, logout, and failed login events
  2. Respond to user creation, updates, and deletions
  3. 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

  1. Last Seen Tracker: Create a system that updates a last_seen field on user profiles whenever they log in or perform an action.

  2. Login History Dashboard: Build a simple admin interface or user dashboard that displays login history using the data collected from user login signals.

  3. IP-Based Security: Extend the failed login handler to temporarily block IPs that have too many failed login attempts.

  4. Custom Welcome Journey: Create a system that uses the user creation signal to start a custom onboarding email sequence for new users.

  5. 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! :)