Django Social Auth and Facebook Canvas Applications: Example

13 июля 2012

Illustrating technology described here I’ve added an example code to DSA in this pull request. Enjoy 🙂

Django Social Auth and Facebook Canvas Applications

22 сентября 2011

This post will be on English in order to be useful for a wider audience of developers.

Signed request authorization

Django Social Auth supports authentication via Facebook by default. When my project required to develop a Facebook application as another interface for our service I’ve got a natural desire to use the same authorization framework we use for login. This would be quite efficient and handy in terms of re-use of existing users database and well-known code. Unfortunately, authentication process for Canvas applications is different comparing to normal OAuth. Looking to Facebook manual you see:

In order to create a personalized user experience, Facebook sends your app information about the user. This information is passed to your Canvas URL using HTTP POST within a single signed_request parameter which contains a base64url encoded JSON object.

I decided to add signed_request authentication to my fork of Django Social Auth. Signed request has to be decoded using your application API secret. Successful decoding means you really deal with Facebook itself. Decoded request contains some info about current user (no matter whether he/she had installed your application or not) and access_token that you should use for Facebook API calls.

So you have a view for your Facebook canvas application (this view should be excluded from CSRF check as I mentioned in the previous post) and you would like to authenticate the user that loaded that page – of course, if this user is already registered or connected the Facebook account to your web site. To do this, you need to decode signed_request, get user Id and access_token from there, and check your users database. This is what you need for that if you use my fork of DSA:

from social_auth.views import complete as social_complete

social_complete(request, FacebookBackend.name)

If the request is Ok, the current user will be authorized via Django authorization system. A new user account will be created if it is not in the user database yet (with internal request for user’s info to Facebook). To check the result of authentication use the code like this:

# Checks the completeness of current user authentication; complete = logged via Facebook backend
def is_complete_authentication(request):
    return request.user.is_authenticated() and FacebookBackend.__name__ in request.session.get(BACKEND_SESSION_KEY, '')

Logging in decorator

It is possible that the user was already logged on your site when he/she opened its Facebook interface. So to proceed with logging you need to check the backend used for authorization of the current user, if it is not Facebook you better re-log the user to Facebook backend automatically. I’ve made a decorator to handle all Facebook-related logging actions. Wrap your Facebook view in this decorator and use usual request.user for authorization-related stuff.

# Facebook decorator to setup environment
def facebook_decorator(func):
    def wrapper(request, *args, **kwargs):

        # User must me logged via FB backend in order to ensure we talk about the same person
        if not is_complete_authentication(request):
            try:
                social_complete(request, FacebookBackend.name)
            except ValueError:
                pass # no matter if failed
        
        # Need to re-check the completion
        if is_complete_authentication(request):
            kwargs.update({'access_token': get_access_token(request.user)})
        else:
            request.user = AnonymousUser()

        signed_request = load_signed_request(request.REQUEST.get('signed_request', ''))
        if signed_request:
            kwargs.update({'signed_request': signed_request})

        return func(request, *args, **kwargs)
        
    return wrapper

Note, that Facebook backend now contains useful function load_signed_request that you can use to read the content of signed request. I store it as another argument for the Facebook view because sometimes it contains valuable info (locale, additional arguments, etc). get_access_token is the function that reads access_token from database or cache for the certain user. It should contain the code like this:

def get_access_token(user):    
    key = str(user.id)
    access_token = cache.get(key)
    
    # If cache is empty read the database
    if access_token is None:
        try:
            social_user = user.social_user if hasattr(user, 'social_user') else UserSocialAuth.objects.get(user=user.id, provider=FacebookBackend.name)
        except UserSocialAuth.DoesNotExist:
            return None
        
        if social_user.extra_data:
            access_token = social_user.extra_data.get('access_token')
            expires = social_user.extra_data.get('expires')
             
            cache.set(key, access_token, int(expires) if expires is not None else 0)
        
    return access_token

Connecting to application

But what if the current user has not connected to your application yet and just browsing? That turned out to be a tricky case. The POST request form Facebook contain only a little piece of information, so user cannot be identified. In this case, you should use OAuth dialog or JavaScript API.

My choice was to use FB.login when the user wants to do an action that required authentication. My template pops application connection dialog, and if the user agrees to install application the page will be reloaded with additional information needed for authentication. The Facebook backend does the rest. The template looks like this:

{% block js %}
<script type="text/javascript">
function startConnect(){
    FB.login(function(response) {
        if (response.authResponse) {
            window.location = window.location +
                                                '?access_token=' + response.authResponse.accessToken +
                                                '&expires=' + response.authResponse.expiresIn +
                                                '&signed_request=' + response.authResponse.signedRequest;
        }

    }, {scope: "{{ app_scope }}" })
}
{% endblock %}

{% block content %}
<div id="fb-root"></div>
<script type="text/javascript">
    window.fbAsyncInit = function() {
        FB.init({appId: {{ fb_app_id }}, status: true, cookie: true, xfbml: true, oauth: true});

        window.setTimeout(function() {
            FB.Canvas.setAutoResize();
        }, 250);
     };

    (function() {
        var e = document.createElement('script'); e.async = true;
        e.src = document.location.protocol +
          '//connect.facebook.net/ru_RU/all.js';
        document.getElementById('fb-root').appendChild(e);
     }());
</script>
{% endblock %}

startConnect is called when you want the user to connect to application. Permissions are taken from your settings that are the same for usual Facebook login via Django Social Auth.

Important note

Original Django Social Auth does not support this tricks so far, so you need to use my fork.

Update

Functionality has been merged to main branch on May 9, 2012.

Facebook iframe-приложения и Django CSRF

3 сентября 2011

Первое, что видят начинающие разработчики iframe-приложений для Facebook при обращении к их canvas view, — сообщение об ошибке CSRF. Связано это с тем, что при запуске iframe-приложения Facebook автоматически выполняет на canvas URL POST-запрос с информацией о текущем пользователе. Странно было бы ожидать в нем правильного CSRF-token. Обычной практикой является полный отказ от CSRF для Facebook. Другой вариант — это написание своих CSRF-токенов и их ручная проверка. Но небольшое исследование показало, что вполне можно пользоваться стандартным механизмом Django, если научиться проверять CSRF не всегда, а только когда это необходимо. А точнее, проверять CSRF стоит лишь в том случае, когда в POST-запросе нет корректного signed_request с access_token — его наличие недвусмысленно говорит нам о том, что POST-запрос пришел от Facebook. Вот как это решилось в нашем случае:

# Description for enlgish-speaking users:
# Easy and standard way to manage CSRF when developing Django applications for Facebook
from django.views.decorators.csrf import csrf_view_exempt
from django.middleware.csrf import CsrfViewMiddleware

# Function to check CSRF on demand (use {% csrf_token %} in your forms as usual)
def facebook_csrf_check(request):
    return CsrfViewMiddleware().process_view(request, facebook_csrf_check, None, None) == None

Функция facebook_csrf_check, собственно, является ручной проверкой на правильность CSRF. Вы можете вызывать ее при необходимости. Автоматическая проверка отключается декоратором csrf_view_exempt. Например:

# Your canvas view
@csrf_view_exempt
def facebook_canvas(request):

    if is_valid_access_token(request): # check whether a correct access_token presents
        ...

    print 'CSRF ' + str(facebook_csrf_check(request)) # facebook_csrf_check == True means CSRF is OK

facebook_csrf_check == True означает, что с csrf все в порядке.

Code snippet.