Django - Facebook Connect integration with ajax (no middleware)

Facebook Connect logo

There is now a github repo for this code at
http://github.com/teebes/django-facebookconnect/

Why do this

I hardly need to prove the point that web authentication is fast becoming a nightmare for users to manage. Obviously Facebook Connect and Open ID are interesting solutions but implementing them is not always straight forward... and yet until every site starts implementing those solutions, the problem will remain. I looked around for a really simple Django solution and found a few but they involved either custom middleware or even a custom auth backend.

So I took at stab at this using only Ajax, jQuery and standard django stuff, with the aim of making the integration as minimalist and non-invasive as possible.

This site's facebook connect implementation started out with exactly the code below. It's a bit more sophisticated now because I had to implement various linking capabilities. If there is any interest in seeing the source code as it currently stands leave a comment and I'll look into it.

Requirements

None! all of this should come standard with Python and Django.

How it works

If a connect login occurs and that facebook ID has never logged in to this site, a standard django.contrib.auth user is created, as well as a custom FacebookUser object that keeps track of the Facebook - Django user ID mappings. If the user has previously logged on, the corresponding Django user is used. The user is then logged in just like any regular Django user.

Setup

Start by creating a facebook application that your site will use for connect.

The important thing here is to get the 'Connect URL' right, which is under the 'Connect' tab. It will be http://<your-project's-address>/facebookconnect

In your django project that will be using connect, setup the following values in your settings.py (or wherever you keep your settings) (replace the xxxxxxxx with the correct values for your setup):

# where the user will go after they log in via facebook
LOGIN_REDIRECT_URL = '/blog/'
# the application api key given by facebook
FACEBOOK_API_KEY = 'xxxxxxxx'
# the applications ecret key given by facebook
FACEBOOK_APPLICATION_SECRET = 'xxxxxxxx'

# add the facebookconnect app to the list of apps.
INSTALLED_APPS = (
    ...
    'facebookconnect',
    ...
)

Include the facebook connect url file to your urls:

urlpatterns = pattern('',
   ...
   (r'^facebookconnect/', include('facebookconnect.urls'),
   ...
)

The facebookconnect Django app

Here is the anatomy of the app. Make sure the app is in your python path or site-packages directory.

facebookconnect/
	__init__.py
	models.py
	views.py
	urls.py
        templates/
                xd_receiver.htm
	templatetags/
		__init__.py
		facebookconnect.py

models.py

from django.contrib.auth.models import User
from django.db import models

class FacebookUser(models.Model):
    facebook_id = models.CharField(max_length=100, unique=True)
    contrib_user = models.OneToOneField(User)
    contrib_password = models.CharField(max_length=100)

views.py

import datetime
import hashlib
import logging

from django.conf import settings
from django.contrib import auth
from django.contrib.auth.models import User
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render_to_response
from django.template import RequestContext

from facebookconnect.models import FacebookUser

def login_facebook_connect(request):
    status = 'unknown failure'
    try:
        expires = request.POST['expires']
        ss = request.POST['ss']
        session_key = request.POST['session_key']
        user = request.POST['user']
        sig = request.POST['sig']

        pre_hash_string = "expires=%ssession_key=%sss=%suser=%s%s" % (
            expires,
            session_key,
            ss,
            user,
            settings.FACEBOOK_APPLICATION_SECRET,
        )
        post_hash_string = hashlib.new('md5')
        post_hash_string.update(pre_hash_string)
        if post_hash_string.hexdigest() == sig:
            try:
                fb = FacebookUser.objects.get(facebook_id=user)
                status = "logged in existing user"
            except FacebookUser.DoesNotExist:
                contrib_user = User()
                contrib_user.save()
                contrib_user.username = u"fbuser_%s" % contrib_user.id

                fb = FacebookUser()
                fb.facebook_id = user
                fb.contrib_user = contrib_user

                temp = hashlib.new('sha1')
                temp.update(str(datetime.datetime.now()))
                password = temp.hexdigest()

                contrib_user.set_password(password)
                fb.contrib_password = password
                fb.save()
                contrib_user.save()
                status = "created new user"

            authenticated_user = auth.authenticate(
                                         username=fb.contrib_user.username, 
                                         password=fb.contrib_password)
            auth.login(request, authenticated_user)
        else:
            status = 'wrong hash sig'

            logging.debug("FBConnect: user %s with exit status %s" % (user, status))

    except Exception, e:
        logging.debug("Exception thrown in the FBConnect ajax call: %s" % e)

    return HttpResponse("%s" % status)

def xd_receiver(request):
        return render_to_response('facebookconnect/xd_receiver.html')

urls.py

from django.conf.urls.defaults import patterns, url, include
from django.views.generic.simple import direct_to_template

urlpatterns = patterns('facebookconnect.views',
    url(r'^xd_receiver\.htm$', direct_to_template, {'template': 'xd_receiver.htm'}, name='xd_receiver'),
    url(r'^login_facebook_connect/$', 'login_facebook_connect', name='facebook_connect_ajax'),
)

templates/xd_receiver.htm

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<body>
    <script src="http://static.ak.connect.facebook.com/js/api_lib/v0.4/XdCommReceiver.js" type="text/javascript"></script>
</body>
</html>

templatetags/facebookconnect.py

from django import template
from django.conf import settings
from django.core.urlresolvers import reverse

register = template.Library()

class FacebookScriptNode(template.Node):
        def render(self, context):
            return """
            <script src="http://static.ak.connect.facebook.com/js/api_lib/v0.4/FeatureLoader.js.php" type="text/javascript"></script>
    
            <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
    
            <script type="text/javascript"> FB.init("%s", "%s");
                function facebook_onlogin() {
                    var uid = FB.Facebook.apiClient.get_session().uid;
                    var session_key = FB.Facebook.apiClient.get_session().session_key;
                    var expires = FB.Facebook.apiClient.get_session().expires;
                    var secret = FB.Facebook.apiClient.get_session().secret;
                    var sig = FB.Facebook.apiClient.get_session().sig;
    
                    fb_connect_ajax(expires, session_key, secret, uid, sig);
    
                }
    
                function fb_connect_ajax(expires, session_key, ss, user, sig) {
        
                    var post_string = 'expires=' + expires;
                    post_string = post_string + '&session_key=' + session_key;
                    post_string = post_string + '&ss=' + ss;
                    post_string = post_string + '&user=' + user;
                    post_string = post_string + '&sig=' + sig;
    
                    $.ajax({
                        type: "POST",
                        url: "%s",
                        data: post_string,
                        success: function(msg) {
                            window.location = '%s'; //.reload()
                        }
                    });
                } 
            </script>       
            """ % (settings.FACEBOOK_API_KEY, reverse('xd_receiver'), reverse('facebook_connect_ajax'), settings.LOGIN_REDIRECT_URL)


def facebook_connect_script(parser, token): return FacebookScriptNode()

register.tag(facebook_connect_script)

class FacebookLoginNode(template.Node):
    def render(self, context): 
        return "<fb:login-button onlogin='facebook_onlogin();'></fb:login-button>"

def facebook_connect_login_button(parser, token): return FacebookLoginNode()

register.tag(facebook_connect_login_button)

Putting it all together

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:fb="http://www.facebook.com/2008/fbml">
<body>

<p>Login via facebook!</p>

{% load facebookconnect %}
{% facebook_connect_login_button %}

{% facebook_connect_script %}

</body>
</html>
3 comments - leave a comment

November 18, 2009 10:10 p.m. by Lawrence

Thanks so much for this. It'll definitely help the FB/Django integration I'm working on now.

July 25, 2010 6:45 a.m. by cedric:

thanks for this post..
I got a question?
the last code title "putting it all together " is supposed to be used in an iframe?

July 25, 2010 10:43 a.m. by teebes:

Hi Ceric,

It can be, but it doesn't have to be. You just need to include those Django tags anywhere you want to display the login button. The html tag does need that xmlns:fb attribute though so make sure to include that in your page.

Leave a comment







Twitter

Jul 28 - subprocess.Popen('<what you actually want>', shell=True, stdout=subprocess.PIPE).communicate() <- so disconcerting every time...

Jul 26 - Something really ironic about how often YouTube videos freeze on Chrome

Jul 26 - @henrymyint is this true when you're observing or authoring the failure? :)