Django's authentication system is somewhat of a black-box, and rightly so - an authentication system needs to be iron-clad. Several times, however, I have wanted to insert additional functionality when a user successfully logs in to a site. I see two ways of accomplishing this:
- Write a custom AUTHENTICATION_BACKEND
- Use signals
- Combine the two!
Custom Backend
Writing a custom backend to 'do some voodoo' is surprisingly painless. Here's a sample implementation:
# settings.py
...
AUTHENTICATION_BACKENDS = ('project.auth_backend.CustomBackend',)
...
# auth_backend.py
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User
class CustomBackend(ModelBackend):
def authenticate(self, username=None, password=None):
try:
user = User.objects.get(username=username)
if user.check_password(password):
**# do some voodoo**
return user
except User.DoesNotExist:
return None
It's clean, it doesn't touch Django, and it does some "custom" authentication. The only downside is that all functionality needs to be built right there or called from right there.
Signals
I see signals as offering an everyman sort of solution. If you want them, they're there. I'm not the first person to want them for login and logout: ticket 5612, which has been around for almost two years, suggests a solution almost identical to the one I've included below. brosner commented that signals are not performant, but I'm inclined to agree more with the next responder, that even if signals are slow-ish login and logout should probably not, in general, see a high volume of traffic. We use comments signals in production on a high-traffic site and have not run into issues there.
Here's my diff:
diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py
index b89aee1..367312f 100644
--- a/django/contrib/auth/__init__.py
+++ b/django/contrib/auth/__init__.py
@@ -1,4 +1,5 @@
import datetime
+**from django.contrib.auth.signals import post_login, post_logout**
from django.core.exceptions import ImproperlyConfigured
from django.utils.importlib import import_module
@@ -54,6 +55,8 @@ def login(request, user):
# TODO: It would be nice to support different login methods, like signed cookies.
user.last_login = datetime.datetime.now()
user.save()
+
+ **post_login.send(sender=None, user=user, request=request) **
if SESSION_KEY in request.session:
if request.session[SESSION_KEY] != user.id:
@@ -75,6 +78,7 @@ def logout(request):
"""
request.session.flush()
if hasattr(request, 'user'):
+ **post_logout.send(sender=None, user=request.user, request=request)**
from django.contrib.auth.models import AnonymousUser
request.user = AnonymousUser()
diff --git a/django/contrib/auth/signals.py b/django/contrib/auth/signals.py
new file mode 100644
index 0000000..f6cbf26
--- /dev/null
+++ b/django/contrib/auth/signals.py
@@ -0,0 +1,4 @@
+**from django.dispatch import Signal
+
+post_login = Signal(providing_args=['user', 'request'])
+post_logout = Signal(providing_args=['user', 'request'])**
To connect to these signals, I would do:
from django.contrib.auth.signals import post_login, post_logout
def login_handler(sender, **kwargs):
**# do some voodoo**
return
post_login.connect(login_handler)
Combination
Instead of forking contrib.auth, why not create a custom backend that sends a signal? The downside to this method is that the request is not available.
#signals.py
from django.dispatch import Signal
post_login = Signal(providing_args=['user'])
#auth_backends.py
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User
from project.signals import post_login
class CustomBackend(ModelBackend):
def authenticate(self, username=None, password=None):
try:
user = User.objects.get(username=username)
if user.check_password(password):
post_login.send(sender=None, user=user)
return user
except User.DoesNotExist:
return None
# listeners.py
from project.signals import post_login
def login_handler(sender, **kwargs):
**# do some voodoo**
return
post_login.connect(login_handler)
View wrapping
I know the title said two approaches - but that was before I thought of this one. I was thinking of how to make the request available. The custom backend -> signal method works well if all you need is a user object, but say you need the request as well. By wrapping the default login and logout views and pointing your login/ & logout/ urls to the wrapped ones, it should be possible to:
- send a signal (or do some voodoo)
- access both the user & the request object
- not fork django
Comments (2)
I saw the signal ticket some time ago and think it's a cool idea. However, I just don't see the benefit of that over a custom backend, which is already in use and is highly customizable.
Peter - yeah, the big thing I ran into was making the actual request available. If all you need is the authenticating user, a custom backend is a good solution.
Commenting has been disabled for this entry
If you'd like to discuss an aspect of this post, feel free to contact me via email.