Unified login in Django and Drupal

I’m working on a project that has two hearts: a Django for a custom CRM with thousands of imported users, and Drupal, where those users should login. This howto apply on:

  • Drupal 7.x (slave)
  • Django 1.5 (master)

There are many single/centralized sign-on solutions around, like OpenID, OAuth CAS and external services like SecurePass to do the job. I’m looking for a more basic solution, and I asked to Django-it Google group for a help.

Here my constraints and requisites:

  • Both Drupal and Django are on the same server
  • Communication between the two is made through a custom TCP API
  • Drush, a command line utility for Drupal, is installed and running
  • Same TLD is used for those sites, eg. http://www.example.com and users.example.com
  • On a Django user creation, a user with the same e-mail is created on Drupal
  • Drupal user id (from now on drupal_id) is stored in the Django user model (a custom user model) after the Django user creation

During the chat in the Django-it group, I come to this solution for the login and logout processes:

Login: Django login -> (auto) Drupal login

Logout: Drupal logout -> (auto) Django logout

Log in to Django

Step 1: The first step is to implement a standard authentication in Django, providing a path for login/logout and two templates for these pages.

Here my template directory structure (template directory is assigned in settings.py):

- base.html
- registration\
-- login.html
-- logged_out.html

You can copy base.html and login.html from the Django core. Here an handy command to find these on command line:

locate admin/base.html | grep django
locate registration/login.html | grep django
locate registration/logged_out.html | grep django

If you have different version of Python and Django installed, check these files are from the right version of Python or you’ll use outdated templates, mine comes in a pretty explicit “/usr/lib/python2.6” directory. Copy those one from the admin directory and make your changes: I suppressed the breadcrumb in the base.html to remove a link to admin backend.

Step 2: In the main urls.py file, I added two login/logout views for the /login and /logout paths using default django views:

urlpatterns = patterns('',
 # ...
 url(r'^login/$', 'django.contrib.auth.views.login'),
 url(r'^logout/$', 'django.contrib.auth.views.logout'),
 # ...
)

Step 3: In settings.py, I add login and logout default urls:

LOGIN_URL = '/login/'
LOGOUT_URL = '/logout/'

You can use everything you want for login and logout paths just changing step 2 and step 3 assignments.

Now http://users.example.com/login/ page should look like this.

djlogin

Automatic Login to Drupal

First, I create a view in my application containing all the logic for the automatic login on Drupal on myapp/views.py.

from django.contrib.auth.decorators import login_required
from django.conf import settings
import subprocess
# ...
@login_required
def sso(request):
    try:
        assert request.user.drupal_id > 0
        # user id to log in
        drupal_id = str(request.user.drupal_id)
        output = ""
        try:
            # DRUPAL_SITE_PATH is the absolute path to Drupal installation
            # DRUPAL_SITE_NAME is the Drupal site name, e.g. example.com
            output = check_output(["drush", "-r", settings.DRUPAL_SITE_PATH, "-l", settings.DRUPAL_SITE_NAME, "user-login", drupal_id])
        except CalledProcessError:
            # @todo add additional control statement?
            pass
        if output:
            # Declare DRUPAL_SITE_URL on settings.py and set it as 'www.example.com'
            #     your Drupal site name
            drupal_login_url = output.replace("http://example-bla-bla.com/", "http://%s/" % settings.DRUPAL_SITE_URL).strip()
            # set Drupal login destination using DRUPAL_LOGIN_DESTINATION
            destination = "%s?destination=%s" % (drupal_login_url, settings.DRUPAL_LOGIN_DESTINATION)
            return redirect(destination)
        else:
            # no output from the drush command
            return HttpResponse('Wrong request')
    except AssertionError:
        # Drupal id is not assigned
        return HttpResponse('Not registered user')

Note: if you’re using Python <= 2.6, you should implement yourself the check_output method:

# subprocess.check_output() for Python <= 2.6
# @see http://stackoverflow.com/a/2924457/892951
def check_output(*popenargs, **kwargs):
    if 'stdout' in kwargs:
        raise ValueError('stdout argument not allowed, it will be overridden.')
    process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs)
    output, unused_err = process.communicate()
    retcode = process.poll()
    if retcode:
        cmd = kwargs.get("args")
        if cmd is None:
            cmd = popenargs[0]
        raise subprocess.CalledProcessError(retcode, cmd, output=output)
    return output

class CalledProcessError(Exception):
    def __init__(self, returncode, cmd, output=None):
        self.returncode = returncode
        self.cmd = cmd
        self.output = output
    def __str__(self):
        return "Command '%s' returned non-zero exit status %d" % (
            self.cmd, self.returncode)
# overwrite CalledProcessError due to `output` keyword might be not available
subprocess.CalledProcessError = CalledProcessError

If you’re using Python 2.7, check_output is already implemented and you can skip this step.

On Django 1.5, an handy settings.py we can redirect a user after login using LOGIN_REDIRECT_URL. Instead of specifying an url, I use the view name I just added:

LOGIN_REDIRECT_URL = 'myapp.views.sso'

What’s going on?

After the successfully login by a valid Django user from http://www.example.com/login/, the user will be redirected to /sso/. The login_required decorator check if a user is logged in and if it’s not the LOGIN_URL page is displayed, with the sso page in the next parameter. So when sso view is displayed, the user is logged in and we can redirect her/him to a Drupal one-time login link we just get from drush on command line:

drush -r /var/www/my/funny/website/root/ -l example.com user-login 2

The output is something like this:

http://example-bla-bla.com/user/reset/2/SECRET/SECRET/login

“2” is drupal_id for the user just logged in.

The whole check_output part is to get this value, clean it and then redirect the user to the right page. The substitution on line 22 is necessary in my case for a drush issue that can display the wrong website name in my Drupal development installation. You can skip it if the url is valid, I suggest you to run drush on command line.

The result is like this:
drupal_loggedin

Logout

The last step is to logout the user on Drupal and then logout from Django. Now this step has to be implemented in Drupal.

So all we have to do is to redirect to this page after a successful logout on Drupal. We can do it via UI using Rules and Rules UI module:

  1. Visit: admin/config/workflow/rules
  2. Add new rule
  3. Reach on event > User > User has logged out
  4. Actions > Add action > System > Page redirect
  5. URL: http://users.example.com/logout/

From now on, after a Drupal user will log out on Drupal via users/logout, her/he will be redirected to the logout page on Django and then logged out.

Side effects

Since we are actually using a one-time login URL, a confusing message is displayed to the unaware user. This message can be changed via String overrides module.

Enable and install module via drush:

drush -r /var/www/my/funny/website/root/ -l example.com dl stringoverrides
drush -r /var/www/my/funny/website/root/ -l example.com en -y stringoverrides

And then visit admin/config/regional/stringoverrides, paste the message you get into the original text field and then add the replacement message.

logged_in_message

Additional steps

Here some additional steps on Drupal, valid for the project I’m working on:

  • Disable Drupal registration and use Django registration instead
  • Disable Drupal login blocks
  • Provide a login url to Django: I’ll use http://users.example.com/sso/ instead of /login/ because I can change the myapp.views.sso it later to not log in users already logged in and because the login_required decorator already serve the login page if the user is not logged in.

These steps may seem trivial but they are important, since if we cannot allow that two different users are logged in in Django and Drupal. A lock to prevent users to log in on Drupal and to register a new account on Drupal should be implemented.

Hope it helps both Drupal and Django users. Happy coding!

3 responses to “Unified login in Django and Drupal”

    • Well, nowadays if you want to do the opposite you can rely on something like OAuth Server module on Drupal, then use something like Authlib on Django side. Never tried, but this path deserve to be deepened to avoid to reinvent the wheel.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: