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