Add a button to Django admin to login as a user (without the password)

Django correctly stores user passwords as md5 hashes by default. This is great for security; there is zero chance that a password could be exposed via flaw in the site, attack, disgruntled employee, whatever. But what if you had a use case where you wanted to login as user without a password?

The use case I have in mind is allowing admin users to login as a user via the Django admin application. This could be very useful for reproducing bugs or verifying what a particular user is seeing. Without knowing the user's password, the only way for an admin to login as them would be to reset their password, login, do their bussiness, and then email the user the new password. Hardly ideal.

Adding a button to the user page in admin is easy. The user model is in the auth application, so all you have to do is create a file called admin/auth/change_form.html in your templates directory. There, you can extend the base change_form.html for the User model. Note: root around in the /usr/lib/pymodules/python2.6/django/contrib/admin/templates directory for an idea of what files you can extend.

{% extends "admin/change_form.html" %}

{% block object-tools %}
{% if change %}{% if not is_popup %}
  <ul class="object-tools">
    <li><a href="history/" class="historylink">History</a></li>
    <li><a href="/login/user/{{ object_id }}?hash={{ 'user'|hash:object_id }}">Login</a></li>
{% endif %}{% endif %}
{% endblock %}

In this case, the relative URL for the login would be the /login/user/$id. If you made the URL absolute, you could provide an alternate domain name, which would allow you a separate cookie, so you could be logged in as different users in both admin and the application at the same time.

What's that hash parameter? It's just a security feature to make sure an attacker cannot access this URL without knowing the secret key. The filter definition looks like this:

from django import template

register = template.Library()  

def hash(type, id):
    hash = hashlib.md5()
    hash.update("%s:%s:%s" % (type, id, settings.ADMIN_HASH_SECRET))
    return hash.hexdigest().upper()

The URL is routed in typical fashion via

    url(r'^login/user/(?P<user_id>[\d_]+)$', admin.login_as_user),

Finally, here is the view that implements the login securely.

from django.conf import settings
from django.http import HttpResponseRedirect
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.contrib.auth import login, authenticate

# the same filter that I called in the template
from search.helpers.tags import logic

def login_as_user(request, user_id):

    # security check; don't let unauthorised people login
    request_hash = request.REQUEST.get("hash", "")
    if request_hash != logic.hash("user", user_id):
        raise Exception("invalid hash value")
    user = User.objects.get(id=user_id)
    # ADMIN_HASH_SECRET is set in, can be any secret string 
    user = authenticate(username=user.username, password=settings.ADMIN_HASH_SECRET)
    login(request, user)    
    return HttpResponseRedirect(reverse("home"))

The Django login() method does the work of logging the user in against whatever backed you have configured, just as if they logged in manually. However, authenticate() is necessary, and by default requires that the actual user's password be passed in. As mentioned previously, this is a big problem because we don't know the user's password; it's stored as a one-way hash.

It turns out to be not such a big problem after all, as Django provides an easy mechanism to extend the authentication module. First, you define your authenticator.

from django.conf import settings
from django.contrib.auth.models import User

class LoginAsUserBackend:
    Allows admins to login as a user without knowing the password.     
    Will authenticate any username, given the password of settings.ADMIN_HASH_SECRET
    def authenticate(self, username=None, password=None):
        if settings.ADMIN_HASH_SECRET != "" and password == settings.ADMIN_HASH_SECRET:
                return User.objects.get(username=username)
        return None

    def get_user(self, user_id):
            return User.objects.get(pk=user_id)
            return None

Django authenticators are called in serial; so my version will be called first, and then if that fails the base Django authenticator will have a go. In my case, I'm allowing any user to login with the secret stored in the settings file. My reasoning is that if they know that secret, they would be able to exploit my new login mechanism anyway.

Then, you just add your new authenticator into the mix in