├── __init__.py ├── django_facebook ├── __init__.py ├── templatetags │ ├── __init__.py │ └── facebook.py ├── templates │ ├── tags │ │ ├── facebook_init.html │ │ └── facebook_load.html │ └── facebook_debug.html ├── auth.py ├── decorators.py └── middleware.py ├── .gitignore ├── setup.py └── README.md /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_facebook/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_facebook/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.pyc 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /django_facebook/templates/tags/facebook_init.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /django_facebook/templates/tags/facebook_load.html: -------------------------------------------------------------------------------- 1 |
2 | 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='django-facebook', 5 | version='0.1', 6 | description='Replace Django Authentication with Facebook', 7 | long_description=open('README.md').read(), 8 | author='Aidan Lister', 9 | author_email='aidan@php.net', 10 | url='http://github.com/aidanlister/django-facebook', 11 | license='MPL', 12 | packages=find_packages(), 13 | include_package_data=True, 14 | zip_safe=False, 15 | install_requires=[], 16 | classifiers=[ 17 | 'Development Status :: 4 - Beta', 18 | 'Environment :: Web Environment', 19 | 'Framework :: Django', 20 | 'Intended Audience :: Developers', 21 | 'Operating System :: OS Independent', 22 | 'Programming Language :: Python', 23 | 'Topic :: Software Development :: Libraries :: Python Modules', 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /django_facebook/templates/facebook_debug.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block facebook_code %} 4 | FB.getLoginStatus(function(response) { 5 | var update = $('#getLoginStatus') 6 | if (response.session) { 7 | update.text('response.session.uid = ' + response.session.uid) 8 | } else { 9 | update.text('response.session = null') 10 | } 11 | }); 12 | {% endblock %} 13 | 14 | 15 | {% block content %} 16 | 17 | 18 | 19 | 20 | 21 | 22 |
request.user{{ request.user }}
request.facebook.uid{{ request.facebook.uid }}
request.facebook.user{{ request.facebook.user }}
request.COOKIES{{ request.COOKIES }}
FB.getLoginStatusLoading ...
23 | 24 | 25 | 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /django_facebook/templatetags/facebook.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.conf import settings 3 | register = template.Library() 4 | 5 | 6 | @register.inclusion_tag('tags/facebook_load.html') 7 | def facebook_load(): 8 | pass 9 | 10 | @register.tag 11 | def facebook_init(parser, token): 12 | nodelist = parser.parse(('endfacebook',)) 13 | parser.delete_first_token() 14 | return FacebookNode(nodelist) 15 | 16 | class FacebookNode(template.Node): 17 | """ Allow code to be added inside the facebook asynchronous closure. """ 18 | def __init__(self, nodelist): 19 | try: 20 | app_id = settings.FACEBOOK_APP_ID 21 | except AttributeError: 22 | raise template.TemplateSyntaxError, "%r tag requires FACEBOOK_APP_ID to be configured." \ 23 | % token.contents.split()[0] 24 | self.app_id = app_id 25 | self.nodelist = nodelist 26 | 27 | def render(self, context): 28 | t = template.loader.get_template('tags/facebook_init.html') 29 | code = self.nodelist.render(context) 30 | custom_context = context 31 | custom_context['code'] = code 32 | custom_context['app_id'] = self.app_id 33 | return t.render(context) 34 | 35 | @register.simple_tag 36 | def facebook_perms(): 37 | return ",".join(getattr(settings, 'FACEBOOK_PERMS', [])) 38 | -------------------------------------------------------------------------------- /django_facebook/auth.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.auth.models import User 3 | from django.contrib.auth.backends import ModelBackend 4 | import facebook 5 | 6 | class FacebookBackend(ModelBackend): 7 | """ Authenticate a facebook user. """ 8 | def authenticate(self, fb_uid=None, fb_graphtoken=None): 9 | """ If we receive a facebook uid then the cookie has already been validated. """ 10 | if fb_uid: 11 | user, created = User.objects.get_or_create(username=fb_uid) 12 | return user 13 | return None 14 | 15 | 16 | class FacebookProfileBackend(ModelBackend): 17 | """ Authenticate a facebook user and autopopulate facebook data into the users profile. """ 18 | def authenticate(self, fb_uid=None, fb_graphtoken=None): 19 | """ If we receive a facebook uid then the cookie has already been validated. """ 20 | if fb_uid and fb_graphtoken: 21 | user, created = User.objects.get_or_create(username=fb_uid) 22 | if created: 23 | # It would be nice to replace this with an asynchronous request 24 | graph = facebook.GraphAPI(fb_graphtoken) 25 | me = graph.get_object('me') 26 | if me: 27 | if me.get('first_name'): user.first_name = me['first_name'] 28 | if me.get('last_name'): user.last_name = me['last_name'] 29 | if me.get('email'): user.email = me['email'] 30 | user.save() 31 | return user 32 | return None 33 | -------------------------------------------------------------------------------- /django_facebook/decorators.py: -------------------------------------------------------------------------------- 1 | import facebook 2 | from functools import update_wrapper, wraps 3 | from django.contrib.auth import REDIRECT_FIELD_NAME 4 | from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest 5 | from django.utils.decorators import available_attrs 6 | from django.utils.http import urlquote 7 | from django.conf import settings 8 | 9 | 10 | def canvas_only(function=None): 11 | """ 12 | Decorator ensures that a page is only accessed from within a facebook application. 13 | """ 14 | def _dec(view_func): 15 | def _view(request, *args, **kwargs): 16 | # Make sure we're receiving a signed_request from facebook 17 | if not request.POST.get('signed_request'): 18 | return HttpResponseBadRequest() 19 | 20 | # Parse the request and ensure it's valid 21 | try: 22 | signed_request = request.POST["signed_request"] 23 | data = facebook.parse_signed_request(signed_request, settings.FACEBOOK_SECRET_KEY) 24 | except ValueError: 25 | return HttpResponseBadRequest() 26 | 27 | # If the user has not authorised redirect them 28 | if not data.get('user_id'): 29 | scope = getattr(settings, 'FACEBOOK_PERMS', None) 30 | auth_url = facebook.auth_url(settings.FACEBOOK_APP_ID, settings.FACEBOOK_CANVAS_PAGE, scope) 31 | markup = '' % auth_url 32 | return HttpResponse(markup) 33 | 34 | # Success so return the view 35 | return view_func(request, *args, **kwargs) 36 | return _view 37 | return _dec(function) 38 | 39 | 40 | def facebook_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME): 41 | """ 42 | Decorator for views that checks that the user is logged in, redirecting 43 | to the log-in page if necessary. 44 | """ 45 | def _passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME): 46 | if not login_url: 47 | from django.conf import settings 48 | login_url = settings.LOGIN_URL 49 | 50 | def decorator(view_func): 51 | def _wrapped_view(request, *args, **kwargs): 52 | if test_func(request): 53 | return view_func(request, *args, **kwargs) 54 | path = urlquote(request.get_full_path()) 55 | tup = login_url, redirect_field_name, path 56 | return HttpResponseRedirect('%s?%s=%s' % tup) 57 | return wraps(view_func, assigned=available_attrs(view_func))(_wrapped_view) 58 | return decorator 59 | 60 | actual_decorator = _passes_test( 61 | lambda r: r.facebook, 62 | redirect_field_name=redirect_field_name 63 | ) 64 | 65 | if function: 66 | return actual_decorator(function) 67 | return actual_decorator 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Facebook integration for your Django website 2 | ============================================= 3 | 4 | Installation: 5 | ------------ 6 | Simply add ``django_facebook`` to your INSTALLED_APPS and configure 7 | the following settings: 8 | 9 | FACEBOOK_APP_ID = '' 10 | FACEBOOK_API_KEY = '' 11 | FACEBOOK_SECRET_KEY = '' 12 | 13 | # Optionally set default permissions to request, e.g: ['email', 'user_about_me'] 14 | FACEBOOK_PERMS = [] 15 | 16 | # And for local debugging, use one of the debug middlewares and set: 17 | FACEBOOK_DEBUG_TOKEN = '' 18 | FACEBOOK_DEBUG_UID = '' 19 | FACEBOOK_DEBUG_COOKIE = '' 20 | FACEBOOK_DEBUG_SIGNEDREQ = '' 21 | 22 | 23 | Templates: 24 | --------- 25 | A few helpers for using the Javascript SDK can be enabled by adding 26 | this to your base template in the ```` section: 27 | 28 | {% load facebook %} 29 | {% facebook_init %} 30 | {% block facebook_code %}{% endblock %} 31 | {% endfacebook %} 32 | 33 | And this should be added just before your ```` tag: 34 | 35 | {% facebook_load %} 36 | 37 | The ``facebook_load`` template tag inserts the code required to 38 | asynchronously load the facebook javascript SDK. The ``facebook_init`` 39 | tag calls ``FB.init`` with your configured application settings. It is 40 | best to put your facebook related javascript into the ``facebook_code`` 41 | region so that it can be called by the asynchronous handler. 42 | 43 | You may find the ``facebook_perms`` tag useful, which takes the setting 44 | in FACEBOOK_PERMS and prints the extended permissions out in a 45 | comma-separated list. 46 | 47 | 49 | 50 | 51 | A helpful debugging page to view the status of your facebook login can 52 | be enabled by adding this to your url configuration: 53 | 54 | (r'^facebook_debug/', direct_to_template, {'template':'facebook_debug.html'}), 55 | 56 | 57 | Once this is in place you are ready to start with the facebook javascript SDK! 58 | 59 | This module also provides all of the tools necessary for working with facebook 60 | on the backend: 61 | 62 | 63 | Middleware: 64 | ---------- 65 | This provides seamless access to the Facebook Graph via request object. 66 | 67 | If a user accesses your site with: 68 | - a valid cookie (Javascript SDK), or 69 | - a valid ``signed_request`` parameter (Facebook Canvas App), 70 | then your views will have access to request.facebook.graph and you can 71 | begin querying the graph immediately. For example, to get the users friends: 72 | 73 | def friends(request): 74 | if request.facebook: 75 | friends = request.facebook.graph.get_connections('me', 'friends') 76 | 77 | To use the middleware, simply add this to your MIDDLEWARE_CLASSES: 78 | 'django_facebook.middleware.FacebookMiddleware' 79 | 80 | 81 | ``FacebookDebugCookieMiddleware`` allows you to set a cookie in your settings 82 | file and use this to simulate facebook logins offline. 83 | 84 | ``FacebookDebugTokenMiddleware`` allows you to set a uid and access_token to 85 | force facebook graph availability. 86 | 87 | ``FacebookDebugCanvasMiddleware`` allows you to set a signed_request to mimic 88 | a page being loaded as a canvas inside Facebook. 89 | 90 | 91 | Authentication: 92 | -------------- 93 | This provides seamless integration with the Django user system. 94 | 95 | If a user accesses your site with a valid facebook cookie, a user 96 | account is automatically created or retrieved based on the facebook UID. 97 | 98 | To use the backend, add this to your AUTHENTICATION_BACKENDS: 99 | 'django_facebook.auth.FacebookBackend' 100 | 101 | To automatically populate your User and Profile models with facebook data, use: 102 | 'django_facebook.auth.FacebookProfileBackend' 103 | 104 | Don't forget to include the default backend if you want to use standard 105 | logins for users as well: 106 | 'django.contrib.auth.backends.ModelBackend' 107 | 108 | 109 | Decorators: 110 | ---------- 111 | ``@facebook_required`` is a decorator which ensures the user is currently 112 | logged in with facebook and has access to the facebook graph. It is a replacement 113 | for ``@login_required`` if you are not using the facebook authentication backend. 114 | 115 | ``@canvas_required`` is a decorater to ensure the view is being loaded with 116 | a valid ``signed_request`` via Facebook Canvas. If signed_request is not found, the 117 | decorator will return a HTTP 400. If signed_request is found but the user has not 118 | authorised, the decorator will redirect the user to authorise. 119 | -------------------------------------------------------------------------------- /django_facebook/middleware.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib import auth 3 | import facebook 4 | import datetime 5 | 6 | 7 | class DjangoFacebook(object): 8 | """ Simple accessor object for the Facebook user. """ 9 | def __init__(self, user): 10 | self.user = user 11 | self.uid = user['uid'] 12 | self.graph = facebook.GraphAPI(user['access_token']) 13 | 14 | 15 | class FacebookDebugCanvasMiddleware(object): 16 | """ Emulates signed_request behaviour to test your applications embedding. 17 | 18 | This should be a raw string as is sent from facebook to the server in the POST 19 | data, obtained by LiveHeaders, Firebug or similar. This should initialised 20 | before FacebookMiddleware. 21 | """ 22 | def process_request(self, request): 23 | cp = request.POST.copy() 24 | request.POST = cp 25 | request.POST['signed_request'] = settings.FACEBOOK_DEBUG_SIGNEDREQ 26 | return None 27 | 28 | 29 | class FacebookDebugCookieMiddleware(object): 30 | """ Sets an imaginary cookie to make it easy to work from a development environment. 31 | 32 | This should be a raw string as is sent from a browser to the server, obtained by 33 | LiveHeaders, Firebug or similar. The middleware takes care of naming the cookie 34 | correctly. This should initialised before FacebookMiddleware. 35 | """ 36 | def process_request(self, request): 37 | cookie_name = "fbs_" + settings.FACEBOOK_APP_ID 38 | request.COOKIES[cookie_name] = settings.FACEBOOK_DEBUG_COOKIE 39 | return None 40 | 41 | 42 | class FacebookDebugTokenMiddleware(object): 43 | """ Forces a specific access token to be used. 44 | 45 | This should be used instead of FacebookMiddleware. Make sure you have 46 | FACEBOOK_DEBUG_UID and FACEBOOK_DEBUG_TOKEN set in your configuration. 47 | """ 48 | def process_request(self, request): 49 | user = { 50 | 'uid':settings.FACEBOOK_DEBUG_UID, 51 | 'access_token':settings.FACEBOOK_DEBUG_TOKEN, 52 | } 53 | request.facebook = DjangoFacebook(user) 54 | return None 55 | 56 | 57 | class FacebookMiddleware(object): 58 | """ Transparently integrate Django accounts with Facebook. 59 | 60 | If the user presents with a valid facebook cookie, then we want them to be 61 | automatically logged in as that user. We rely on the authentication backend 62 | to create the user if it does not exist. 63 | 64 | We do not want to persist the facebook login, so we avoid calling auth.login() 65 | with the rationale that if they log out via fb:login-button we want them to 66 | be logged out of Django also. 67 | 68 | We also want to allow people to log in with other backends, which means we 69 | need to be careful before replacing request.user. 70 | """ 71 | 72 | def get_fb_user_cookie(self, request): 73 | """ Attempt to find a facebook user using a cookie. """ 74 | fb_user = facebook.get_user_from_cookie(request.COOKIES, 75 | settings.FACEBOOK_APP_ID, settings.FACEBOOK_SECRET_KEY) 76 | if fb_user: 77 | fb_user['method'] = 'cookie' 78 | return fb_user 79 | 80 | def get_fb_user_canvas(self, request): 81 | """ Attempt to find a user using a signed_request (canvas). """ 82 | fb_user = None 83 | if request.POST.get('signed_request'): 84 | signed_request = request.POST["signed_request"] 85 | try: 86 | data = facebook.parse_signed_request(signed_request, settings.FACEBOOK_SECRET_KEY) 87 | except ValueError: 88 | pass 89 | if data and data.get('user_id'): 90 | fb_user = data['user'] 91 | fb_user['method'] = 'canvas' 92 | fb_user['uid'] = data['user_id'] 93 | fb_user['access_token'] = data['oauth_token'] 94 | return fb_user 95 | 96 | def get_fb_user(self, request): 97 | """ Return a dict containing the facebook user details, if found. 98 | 99 | The dict must contain the auth method, uid, access_token and any 100 | other information that was made available by the authentication 101 | method. 102 | """ 103 | fb_user = None 104 | methods = ['get_fb_user_cookie', 'get_fb_user_canvas'] 105 | for method in methods: 106 | fb_user = getattr(self, method)(request) 107 | if (fb_user): 108 | break 109 | return fb_user 110 | 111 | def process_request(self, request): 112 | """ Add `facebook` into the request context and attempt to authenticate the user. 113 | 114 | If no user was found, request.facebook will be None. Otherwise it will contain 115 | a DjangoFacebook object containing: 116 | uid: The facebook users UID 117 | user: Any user information made available as part of the authentication process 118 | graph: A GraphAPI object connected to the current user. 119 | 120 | An attempt to authenticate the user is also made. The fb_uid and fb_graphtoken 121 | parameters are passed and are available for any AuthenticationBackends. 122 | 123 | The user however is not "logged in" via login() as facebook sessions are ephemeral 124 | and must be revalidated on every request. 125 | """ 126 | fb_user = self.get_fb_user(request) 127 | request.facebook = DjangoFacebook(fb_user) if fb_user else None 128 | 129 | if fb_user and request.user.is_anonymous(): 130 | user = auth.authenticate(fb_uid=fb_user['uid'], fb_graphtoken=fb_user['access_token']) 131 | if user: 132 | user.last_login = datetime.datetime.now() 133 | user.save() 134 | request.user = user 135 | return None 136 | --------------------------------------------------------------------------------