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.
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 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:
“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 last step is to logout the user on Drupal and then logout from Django. Now this step has to be implemented in Drupal.
- Logout url is: http://users.example.com/logout/
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:
- Visit: admin/config/workflow/rules
- Add new rule
- Reach on event > User > User has logged out
- Actions > Add action > System > Page redirect
- 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.
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.
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!