On my site, I have some resources that are private, such as certain blog entries and photos. I wanted to make it so that my Facebook friends could log into my site without me having to create Django-based accounts for them. It turned out to be fairly trivial to get things set up, but I thought I'd share what I did in case it can be of use to anyone else.


Set up Facebook Connect

Follow the Basic guide from the Facebook Developers WIki. For the cross-domain communication channel file step, you could use a flat page or set up a custom URL and template. I opted to set up a custom URL and template. Create a new template in your templates directory named xd_receiver.html:
<!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>
Set up a view for this page by adding the following to views.py:
def xd_receiver(request):
    return render_to_response('xd_receiver.html')
Configure a URL to serve this page by adding the appropriate values to urls.py:
from myproject.views import xd_receiver
...
(r'^xd_receiver\.html$', xd_receiver),
After completing the rest of the Facebook guide, you should be able to log in using the Facebook Connect button on the page where you added the
<fb:login-button>
code.


Adding FacebookConnectMiddleware

I chose to use custom middleware to look for the presence of the Facebook Connect cookies that are set when a user has logged in to Facebook Connect on my site. This middleware does the following: Create a new middleware file namedFacebookConnectMiddleware.py and place the following code in it.
# FacebookConnectMiddleware.py
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import User
from django.conf import settings

import md5
import urllib
import time
import simplejson
from datetime import datetime

# These values could be placed in Django's project settings
API_KEY = 'YOUR KEY FROM FACEBOOK'
API_SECRET = 'YOUR SECRET FROM FACEBOOK'

REST_SERVER = 'http://api.facebook.com/restserver.php'

# You can get your User ID here: http://developers.facebook.com/tools.php?api
MY_FACEBOOK_UID = 'YOUR FACEBOOK User ID'

NOT_FRIEND_ERROR = 'You must be my Facebook friend to log in.'
PROBLEM_ERROR = 'There was a problem. Try again later.'
ACCOUNT_DISABLED_ERROR = 'Your account is not active.'
ACCOUNT_PROBLEM_ERROR = 'There is a problem with your account.'

class FacebookConnectMiddleware(object):
    
    delete_fb_cookies = False
    facebook_user_is_authenticated = False
    
    def process_request(self, request):
        try:
             # Set the facebook message to empty. This message can be used to dispaly info from the middleware on a Web page.
            request.facebook_message = None

            # Don't bother trying FB Connect login if the user is already logged in
            if not request.user.is_authenticated():
            
                # FB Connect will set a cookie with a key == FB App API Key if the user has been authenticated
                if API_KEY in request.COOKIES:

                    signature_hash = self.get_facebook_signature(request.COOKIES, True)
                
                    # The hash of the values in the cookie to make sure they're not forged
                    if(signature_hash == request.COOKIES[API_KEY]):
                
                        # If session hasn't expired
                        if(datetime.fromtimestamp(float(request.COOKIES[API_KEY+'_expires'])) > datetime.now()):

                            # Make a request to FB REST(like) API to see if current user is my friend
                            are_friends_params = {
                                'method':'Friends.areFriends',
                            	'api_key': API_KEY,
                            	'session_key': request.COOKIES[API_KEY + '_session_key'],
                            	'call_id': time.time(),
                            	'v': '1.0',
                            	'uids1': MY_FACEBOOK_UID,
                            	'uids2': request.COOKIES[API_KEY + '_user'],
                            	'format': 'json',
                            }
                
                            are_friends_hash = self.get_facebook_signature(are_friends_params)
    
                            are_friends_params['sig'] = are_friends_hash
                
                            are_friends_params = urllib.urlencode(are_friends_params)
                
                            are_friends_response  = simplejson.load(urllib.urlopen(REST_SERVER, are_friends_params))
                        
                            # If we are friends
                            if(are_friends_response[0]['are_friends'] is True):
                    
                                try:
                                    # Try to get Django account corresponding to friend
                                    # Authenticate then login (or display disabled error message)
                                    django_user = User.objects.get(username=request.COOKIES[API_KEY + '_user'])
                                    user = authenticate(username=request.COOKIES[API_KEY + '_user'], 
                                                        password=md5.new(request.COOKIES[API_KEY + '_user'] + settings.SECRET_KEY).hexdigest())
                                    if user is not None:
                                        if user.is_active:
                                            login(request, user)
                                            self.facebook_user_is_authenticated = True
                                        else:
                                            request.facebook_message = ACCOUNT_DISABLED_ERROR
                                            self.delete_fb_cookies = True
                                    else:
                                       request.facebook_message = ACCOUNT_PROBLEM_ERROR
                                       self.delete_fb_cookies = True
                                except User.DoesNotExist:
                                    # There is no Django account for this Facebook user.
                                    # Create one, then log the user in.
                    
                                    # Make request to FB API to get user's first and last name
                                    user_info_params = {
                                        'method': 'Users.getInfo',
                                    	'api_key': API_KEY,
                                    	'call_id': time.time(),
                                    	'v': '1.0',
                                    	'uids': request.COOKIES[API_KEY + '_user'],
                                    	'fields': 'first_name,last_name',
                                    	'format': 'json',
                                    }

                                    user_info_hash = self.get_facebook_signature(user_info_params)

                                    user_info_params['sig'] = user_info_hash
                    
                                    user_info_params = urllib.urlencode(user_info_params)

                                    user_info_response  = simplejson.load(urllib.urlopen(REST_SERVER, user_info_params))
                    
                    
                                    # Create user
                                    user = User.objects.create_user(request.COOKIES[API_KEY + '_user'], '', 
                                                                    md5.new(request.COOKIES[API_KEY + '_user'] + 
                                                                    settings.SECRET_KEY).hexdigest())
                                    user.first_name = user_info_response[0]['first_name']
                                    user.last_name = user_info_response[0]['last_name']
                                    user.save()
                    
                                    # Authenticate and log in (or display disabled error message)
                                    user = authenticate(username=request.COOKIES[API_KEY + '_user'], 
                                                        password=md5.new(request.COOKIES[API_KEY + '_user'] + settings.SECRET_KEY).hexdigest())
                                    if user is not None:
                                        if user.is_active:
                                            login(request, user)
                                            self.facebook_user_is_authenticated = True
                                        else:
                                            request.facebook_message = ACCOUNT_DISABLED_ERROR
                                            self.delete_fb_cookies = True
                                    else:
                                       request.facebook_message = ACCOUNT_PROBLEM_ERROR
                                       self.delete_fb_cookies = True
                            # Not my FB friend
                            else:
                                request.facebook_message = NOT_FRIEND_ERROR
                                self.delete_fb_cookies = True
                            
                        # Cookie session expired
                        else:
                            logout(request)
                            self.delete_fb_cookies = True
                        
                   # Cookie values don't match hash
                    else:
                        logout(request)
                        self.delete_fb_cookies = True
                    
            # Logged in
            else:
                # If FB Connect user
                if API_KEY in request.COOKIES:
                    # IP hash cookie set
                    if 'fb_ip' in request.COOKIES:
                        
                        try:
                            real_ip = request.META['HTTP_X_FORWARDED_FOR']
                        except KeyError:
                            real_ip = request.META['REMOTE_ADDR']
                        
                        # If IP hash cookie is NOT correct
                        if request.COOKIES['fb_ip'] != md5.new(real_ip + API_SECRET + settings.SECRET_KEY).hexdigest():
                             logout(request)
                             self.delete_fb_cookies = True
                    # FB Connect user without hash cookie set
                    else:
                        logout(request)
                        self.delete_fb_cookies = True
                        
        # Something else happened. Make sure user doesn't have site access until problem is fixed.
        except:
            request.facebook_message = PROBLEM_ERROR
            logout(request)
            self.delete_fb_cookies = True
        
    def process_response(self, request, response):        
        
        # Delete FB Connect cookies
        # FB Connect JavaScript may add them back, but this will ensure they're deleted if they should be
        if self.delete_fb_cookies is True:
            response.delete_cookie(API_KEY + '_user')
            response.delete_cookie(API_KEY + '_session_key')
            response.delete_cookie(API_KEY + '_expires')
            response.delete_cookie(API_KEY + '_ss')
            response.delete_cookie(API_KEY)
            response.delete_cookie('fbsetting_' + API_KEY)
    
        self.delete_fb_cookies = False
        
        if self.facebook_user_is_authenticated is True:
            try:
                real_ip = request.META['HTTP_X_FORWARDED_FOR']
            except KeyError:
                real_ip = request.META['REMOTE_ADDR']
            response.set_cookie('fb_ip', md5.new(real_ip + API_SECRET + settings.SECRET_KEY).hexdigest())
        
        # process_response() must always return a HttpResponse
        return response
                                
    # Generates signatures for FB requests/cookies
    def get_facebook_signature(self, values_dict, is_cookie_check=False):
        signature_keys = []
        for key in sorted(values_dict.keys()):
            if (is_cookie_check and key.startswith(API_KEY + '_')):
                signature_keys.append(key)
            elif (is_cookie_check is False):
                signature_keys.append(key)

        if (is_cookie_check):
            signature_string = ''.join(['%s=%s' % (x.replace(API_KEY + '_',''), values_dict[x]) for x in signature_keys])
        else:
            signature_string = ''.join(['%s=%s' % (x, values_dict[x]) for x in signature_keys])
        signature_string = signature_string + API_SECRET

        return md5.new(signature_string).hexdigest()
You will then need to tell Django to use this middleware. Add the following to MIDDLEWARE_CLASSES in settings.py:
'myproject.FacebookConnectMiddleware.FacebookConnectMiddleware',
At this point, users should be able to log into your Django-powered site using Facebook connect.


Displaying messages from the middleware (optional)

As you can see, the middleware also sets the
facebook_message
variable in the
HttpRequestObject
to pass error messages to templates. If you want to display these messages on a template, you'll need to update your view and the template. Add the following to your view:
if request.facebook_message is not None:
    facebook_message = request.facebook_message
else:
    facebook_message = ''
Then pass facebook_message to
render_to_response
. Then, in the template, add:
{% if facebook_message %}{{ facebook_message }}{% endif %}
You can update the middleware to change the default messages or to add new messages where desired.

Logging out

This link will log the user out of Facebook via JavaScript, then redirect them to the Django logout (assuming your logout url is configured to be /accounts/logout).
    <a id="logout" href="/accounts/logout" onclick='FB.Connect.ifUserConnected(null,function() { window.location = "/accounts/logout" }); FB.Connect.logoutAndRedirect("/accounts/logout"); return false;'>Sign out</a>



Comments

#1

ricky
December 15, 2008
8:45 p.m.

Just what I was looking for. Pretty awesome, thanks!

 
#2

omtv
December 17, 2008
9:04 a.m.

Yes, quite useful script! Thanks a lot.

 
#3

Srik
December 17, 2008
9:33 a.m.

Thanks, will try this out in next project.

 
#4

Daniel Nyström
December 17, 2008
11:24 a.m.

Why not just use “direct_to_template” in urls.py?

 
#5

Raisins
December 17, 2008
2:19 p.m.

Thanks for the middleware I was in the process of writing the same thing.

 
#6

Andrew
December 17, 2008
3:15 p.m.

You could also use the pyfacebook library to remove a lot of code.

 
#7

Julian
December 17, 2008
3:44 p.m.

Oh yeah, does pyfacebook have Facebook Connect?

 
#8

Bret
December 17, 2008
6:09 p.m.

@ Julian: Oh yeah, does pyfacebook have Facebook Connect?

I think Andrew was referring to my code that made calls to the Facebook API. If I’d used PyFacebook, I wouldn’t have needed to construct, make and parse the API interactions.

 
#9

Julian
December 18, 2008
10:03 a.m.

That’s cool then. Any chance we get some updated source code?

 
#10

Mark
February 4, 2009
6:55 p.m.

Bret Thanks for posting this snippet. This works great - even with a couple of my hacks. I did notice that to fully clear the cookies on logout - you have to logout of the Facebook account AND close the browser - or the middleware logs the user back in to the django session (not really sure it matters - but still… ). Any ideas how to unlink the fb cookie(s) from the django session without closing out the browser. I’m sure there is a way (after a bit of head scratching) but thought you might have run into this?

Thanks. Nice code.

 
#11

Bret
February 5, 2009
7:17 a.m.

I’ve added my logout link code to the end of the entry.

It logs the user out of Facebook via JavaScript, then redirects to Django’s logout page which logs the user out of Django.

 
#12

Jermaine
February 15, 2009
9:42 p.m.

Have not had a chance to test yet, but thanks so much for sharing. Almost finishing building a small app and wanted to use FBConnect. Love the idea that you can restrict login to users who are your friends as well.

 
#13

alen
February 19, 2009
5:25 a.m.

I guess you’re aware of it, but still the remark, there’s a django application that does this too. Check out http://code.google.com/p/django-fbcon... - I’ll update the repository the next few days so existing users can connect their account with their facebook account - and new users can actually pick a username. :)

Cheers, alen

 
#14

Danny
February 28, 2009
8:20 p.m.

My app uses Facebook Connect a little different. I created a Custom Authentication for it.

see http://blog.dannyroa.com/2009/02/24/custom-authentication-in-django-using-facebook-connect/

 
#15

ROb
March 26, 2009
11:03 p.m.

Hey,

I guess I’m late to the game here, I’ve been trying to utilize the middleware here. Something I want to mention is that the facebook tutorial now uses xd_receiver.htm, and no longer .html! So watch out when you set your URLS.PY to html$ you’ll never find it!

Secondly, the reason I am posting, is that I’ve been able to make the connection, I get a facebook cookie and I get my sites own cookie — but then it doesn’t make any sense as to how it knows what my login is.. shouldn’t it have created a new user account?

I’m checking my users table, and it didn’t make one, nor am I logged in, but my cookie is there never the less.

 
#16

Bret
March 30, 2009
8:30 p.m.

Rob,

This middleware should be creating a new user whose username is the same as your friend’s Facebook ID (e.g. 1233443).

I’m not sure why you wouldn’t see the user in your users table if you logged in via Facebook Connect.

Are you logging in as one of your friends, or are you logging in as yourself?

By default, this implementation only allows your friend to log in, and you can’t be your own friend on Facebook. So, if you’re logging in to Facebook Connect as yourself, maybe you’re able to access your admin section because you’re still logged in using your Django-based account.

If you’re able to get to the admin section, you’re logged in not with your Facebook account, but with your Django account.

Ask one of your friends to log in using FB Connect and see if they show up in the users table.

 
#17

Morgan
May 19, 2009
2:11 p.m.

I was playing with a php version to connect my app to facebook, but this seems pretty simple. I’m getting a lot of errors with php, and of course none of the error messages are helpful in php.

 
#18

Susan
June 12, 2009
6:27 p.m.

I’ve been looking for code that works with the Facebook API, connecting Facebook to some other CMS or software system. I will have to play around with this since I have not yet found a solution I am happy with (yet).

 

Post a comment

Markdown accepted here.

Your e-mail address will not be shown publicly.