Hooking into django’s login and logout – two approaches

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:

  1. Write a custom AUTHENTICATION_BACKEND
  2. Use signals
  3. Combine the two!

Custom Backend

Writing a custom backend to ‘do some voodoo’ is surprisingly painless. Here’s a sample implementation:

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

syntax:python
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.

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

Further reading:

Read full article at “charlesleifer.com: Entries tagged with "django"”

Leave a comment