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:
- Looks for the cookies set when a Facebook Connect user logs in
- Verifies the cookies
- Makes sure the Facebook Connect session hasn't expired
- Checks to see if the current user is your Facebook friend
- Logs the user in
- If the user does not have a Django account, one is created.
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
ricky
December 15, 2008
8:45 p.m.
Just what I was looking for. Pretty awesome, thanks!
omtv
December 17, 2008
9:04 a.m.
Yes, quite useful script! Thanks a lot.
Srik
December 17, 2008
9:33 a.m.
Thanks, will try this out in next project.
Daniel Nyström
December 17, 2008
11:24 a.m.
Why not just use “direct_to_template” in urls.py?
Raisins
December 17, 2008
2:19 p.m.
Thanks for the middleware I was in the process of writing the same thing.
Andrew
December 17, 2008
3:15 p.m.
You could also use the pyfacebook library to remove a lot of code.
Julian
December 17, 2008
3:44 p.m.
Oh yeah, does pyfacebook have Facebook Connect?
Bret
December 17, 2008
6:09 p.m.
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.
Julian
December 18, 2008
10:03 a.m.
That’s cool then. Any chance we get some updated source code?
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.
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.
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.
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
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/
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.
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.
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.
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.