Django Social Auth и авторизация в приложениях для ВКонтакте: пример

23 августа 2012

Пример добавлен в мой fork и в главную ветку вот в этом pull request.

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 Auh и авторизация в приложениях для ВКонтакте

8 ноября 2011

Кроме авторизации в приложениях для Facebook в ходе работы над проектом была решена еще одна задача – авторизация в приложениях для ВКонтакте. Как и в случае с Facebook, у меня было сильное желание не делать два раза одну и ту же работу, а использовать одну и ту же базу пользователей и один и тот же механизм для авторизации через сайт ВКонтакте и для авторизации в iframe-приложении ВКонтакте. Это удалось решить без особых сложностей, и вот наконец пришло время сорвать покров тайны с некоторых загадочных настроек.

Особенности ВКонтакте

В отличие от Facebook, у ВКонтакте есть одна принципиальная особенность, которая несколько затрудняет решение этой задачи, а именно: вы не можете использовать одно и то же приложение для авторизации через OAuth и для авторизации в iframe-приложении. Вам необходимо зарегистрировать iframe-приложение для ВКонтакте отдельно и получить для него id и secret.

Другая особенность, которая, наоборот, упрощает работу, заключается в том, что ВКонтакте не делает POST-запрос при переходе на URL вашего приложения, а использует метод GET. То есть, минус одна заморочка с CSRF.

В целом для ВКонтакте используется тот же принцип, что и для Facebook, а именно:

  1. При обращении к view приложения происходит проверка: через какой backend авторизован пользователь;
  2. Если это ВКонтакте, то мы выбираем из базы access_token и используем его для работы с API;
  3. Если нет – мы используем полученный запрос для авторизации;
  4. Доработанный Backend для ВКонтакте разбирает запрос и производит авторизацию с учетом того, что могла использоваться как авторизация через сайт, так и авторизация через приложение.

Начнем по порядку.

Проверка авторизации

Чтобы убедиться, что пользователь нашего view авторизован через нужный нам backend, используется функция is_complete_authorization

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

Декоратор для view приложения

Декоратор выглядит чуть проще, чем для Facebook, так как не нужно разбирать signed_request.

def vkontakte_intro(func):
    def wrapper(request, *args, **kwargs):

        # User must me logged via VKontakte backend in order to ensure we talk about the same person
        if not is_complete_authentication(request):
            try:

                social_complete(request, VKontakteOAuth2Backend.name)
            except (ValueError, AttributeError):
                pass

        # Need to re-check the completion
        if is_complete_authentication(request):
            kwargs.update({'access_token': get_access_token(request.user)})
        else:
            request.user = AnonymousUser()

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

    return wrapper

Назначение у функции get_access_token: вытащить из базы или из кэша access_token для указанного пользователя. Пример:

VK_AT_CACHE_PREFIX = 'VK_AT_%s'

# Returns cached access token for the user; loads it from db if needed
def get_access_token(user):
    key = VK_AT_CACHE_PREFIX % 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=VKontakteOAuth2Backend.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

Подключение к приложению

Для работы некоторых методов API вы должны запросить у пользователя установку вашего приложения и нужные вам права. Это две разные операции, первая из которых запускается при вызове функции startConnect, а вторая – следом за ней, в функции requestRights.

{% block head %}
<script src="http://vkontakte.ru/js/api/xd_connection.js?2" type="text/javascript"></script>

{% endblock %}

{% block js %}
<script type="text/javascript">

VK.init(function() {
// any of your code here
}
    );

    function startConnect() {
        VK.callMethod('showInstallBox');
    }

    function requestRights() {
        VK.callMethod('showSettingsBox', 1 + 2); // 1+2 is just an example
    }

    function onSettingsChanged(settings) {
        window.location.reload();
    }

    $(document).ready( function(){
        VK.addCallback("onApplicationAdded", requestRights);
        VK.addCallback("onSettingsChanged", onSettingsChanged);
    });
</script>
{% endblock %}

Необходимые настройки

Для того чтобы авторизация через приложение начала работать, нужно сделать следующие настройки:

VKONTAKTE_APP_AUTH={'key':'iframe_app_secret_key', 'user_mode': 2, 'id':'iframe_app_id'}

Параметр user_mode может принимать значения 0, 1 или 2, он влияет на проверку того, подключился пользователь к вашему приложению или нет. Если поставить user_mode в 0, то никакой проверки произведено не будет и авторизация успешно пройдет в любом случае. При user_mode=1 будет проведена проверка на значение параметра is_app_user. Этот параметр приходит в GET-запросе, который присылает ВКонтакте на URL вашего приложения при начале работы с ним. Авторизация будет успешной, только если пользователь подключен к вашему приложению. Однако может случиться так, что пользователь подключился к вашему приложению позже, а запрос уже был проанализирован, is_app_user был 0, а в браузере пользователь уже давно ушел на другую страницу. Для этого используется значение 2, которое делает запрос к API и проверяет, подключил пользователь ваше приложение или нет. Лично я пользуюсь последним вариантом.

Значение key используется для проверки авторизации, id – для выполнения запросов к API. Оно имеет более высокий приоритет по отношению к VKONTAKTE_APP_ID, потому что именно для него вы получаете права от пользователя в интерфейсе.

Если вы не хотите использовать авторизацию через приложение, просто не используйте VKONTAKTE_APP_AUTH совсем.

Update

Пример использования добавлен в главную ветку 23 августа 2012.

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.

Django Social Auth: now with images

16 августа 2011

В рамках текущего проекта возникла задача при регистрации пользователей через социальную сеть кроме их контактных данных получать еще и изображение. Небольшое расследование показало, что каждая сеть практикует свой метод отдачи изображений, что в результате вылилось в следующий код, который вызывается в обработчике сигнала pre_update:

from social_auth.signals import pre_update

@receiver(pre_update)
def update_person_details(sender, **kwargs):
    person = kwargs.get('user')
    details = kwargs.get('details')

…

    load_person_avatar(sender, person, kwargs.get('response'))
…

def load_person_avatar(sender, person, info):    
    image_url = None
    
    if sender.name == 'vkontakte-oauth2':
        vk_response = info.get('response')
        if vk_response:            
            image_url = vk_response.get('user_photo') # If photo is absent user_photo is absent too
    
    elif sender.name == 'odnoklassniki':
        image_url = info.get('pic_2')
        if 'stub' in image_url: # No real image
            image_url = None
    
    elif sender.name == 'mailru-oauth2':
        if info.get('has_pic'):
            image_url = info.get('pic_big')
        
    elif sender.name == 'twitter':
        image_url = info.get('profile_image_url')
        if not 'default_profile' in image_url:
            image_url = image_url.replace('_normal', '_bigger')
        else: # No real image
            image_url = None

    elif sender.name == 'yandex-oauth2':
        image_url = info.get('userpic')
    
    elif sender.name == 'facebook':
        image_url = 'http://graph.facebook.com/%s/picture?type=large' % info.get('id')
    
    if image_url:
        try:
            image_content = urlopen(image_url)
            
            # Facebook default image check
            if sender.name == 'facebook' and 'image/gif' in str(image_content.info()):
                return
            
            image_name = default_storage.get_available_name(person.avatar.field.upload_to + '/' + str(person.id) + '.' + image_content.headers.subtype)
            person.avatar.save(image_name, ContentFile(image_content.read()))
            person.save()
        except Exception:
            pass # Here we completely do not care about errors

Примечания:

  1. В качестве backend для Вконтакте был выбран OAuth2 вариант — он в отличие от OpenAPI предоставляет информацию об изображении
  2. В качестве библиотеки для поля изображения мы испольуем sorl.thumbnail

Новости Django Social Auth — 2

30 апреля 2011

Предыдущие новости см. здесь.

В коммите добавлены:

  1. Поддержка OAuth 2.0 для ВКонтакте
  2. Поддержка OAuth 2.0 для Mail.ru
  3. Решена проблема с return_to. Одна строчка правок потребовала усилий со стороны четырех человек, вот как бывает.

Следующий коммит будет не раньше середины мая: уезжаю в отпуск.

Новости Django Social Auth

19 апреля 2011

В продолжение статьи «Авторизация в социальных сетях для Django».

Основная ветка проекта развивается довольно интенсивно, добавилась поддержка LinkedIn, Google OAuth2, тесты и большое количество мелких, но полезных фитюлечек, вроде автоматического ассоциирования пользователя по e-mail.

Замечена интересная особенность Google OpenID. Если ваш сайт поддерживает «сквозную» авторизацию на поддоменах и вам принципиально, чтобы человек, авторизовавшийся в одном поддомене, мог продолжать работать без авторизации и в другом, я рекомендую воспользоваться Google OAuth. Фишка в том, что Google OpenID выдает разные id пользователя для разных поддоменов одного и того же сайта. Так как библиотека не может определить, что пользователь, который пришел на поддомен ААА, уже был зарегистрирован в поддомене БББ, в результате создается новая учетная запись. У OAuth такой проблемы нет, зато есть два плюса: вы гарантированно получаете e-mail пользователя и вам не нужно регистрировать свое приложение, как в OAuth 2.0.

Обнаружена ошибка в библиотеке Python-OpenID, которая выдает довольно странную диагностику: OpenID authentication failed: return_to does not match return URL, при этом URL’ы совпадают. Обсуждение находится здесь, авторам библиотеки сообщение отправлено, ждем, когда поправят.

В моем форке два изменения:

  1. Для авторизации на Yandex теперь не обязательно указывать имя пользователя. Если оно не задано, то используются возможности OpenID 2.0, сервер с библиотекой договариваются сами и, когда надо, задают пользователю необходимые вопросы. Спасибо Ивану Сагалаеву за подсказку (детали обсуждения здесь).
  2. Добавлен backend для авторизации на Одноклассниках с использованием OAuth 2.0. По умолчанию запрашиваемый scope минимален, этого хватает для авторизации, а пользователю не нужно мучительно размышлять, давать ли доступ к своей стене, e-mail и пр.

Внимание, вопрос: поддержку какого именно сервиса авторизации вам бы хотелось увидеть следующей: ВКонтакте через OAuth 2.0 или Mail.ru?

Авторизация в социальных сетях для Django

22 января 2011

Зачем

Когда в нашем проекте возникла необходимость реализовать авторизацию через социальные сети, я по совету Ромы Ворушина просмотрел несколько готовых решений, чтобы не изобретать уже многократно изобретенный велосипед. Оказалось, что не все велосипеды умеют ездить туда, куда нам нужно, и в точности так, как именно нам нужно:

  1. Кроме крупных мировых сетей хотелось также работать с LiveJournal, Yandex и ВКонтакте.
  2. Нужна была возможность контролировать действия системы после успешной авторизации, то есть управлять процессом обработки полученной от сервиса информации о пользователях.

Я остановился на https://github.com/omab/django-social-auth: просмотрев исходники, я увидел, что их немного, а написано все понятно. Изначально библиотека поддерживала Facebook, Twitter, Google, OpenID и OpenAuth и процесс работы с БД был немного не такой, как хотелось бы. Форка было не избежать, правда, в скором времени мы с автором исходной библиотеки немного подружились, в результате чего большая часть моих изменений ушла в основную ветку, а в моем форке остались только Yandex и ВКонтакте.

После всех изменений результат мне пришелся очень по сердцу: в случае необходимости можно просто добавлять движки для новых сервисов, а работа с БД достаточно проста, чтобы не слишком о ней думать. Также вне зависимости от того, как происходит авторизация (о, иногда это достаточно замысловатый процесс, поверьте мне на слово), работа с данными пользователя унифицирована, что тоже очень удобно.

Как это работает

Для каждого сервиса есть два класса – auth и backend. Напрямую вы с ними работаете, только если вам нужно добавить свой сервис, в противном случае вы просто вызываете метод auth библиотеки и скармливаете ему имя сервиса и нужные параметры. После того как авторизация завершена, вызывается метод complete. Что происходит внутри, описано ниже.

1. Класс auth отвечает за авторизацию на сайте сервиса и убеждается, что все прошло успешно (см. рисунок).

2. Класс backend отвечает за авторизацию собственно в вашем приложении (через стандартный authenticate) и за работу с таблицей пользователей на основании тех данных, которыми сервис соизволил с вами поделиться. Некоторые сервисы (не будем показывать пальцем, хотя это был ЖЖ) не дают никакой информации, кроме «наш человек/не наш человек».

Для backend можно задавать ту модель пользователя, которая используется в вашем приложении, если вы не используете стандартный класс User. В менеджере этой модели можно, например, принимать решение, когда создавать нового пользователя, а когда – использовать уже имеющегося.

Для того чтобы понять, нужно ли обновлять информацию в таблице пользователей, используя данные, полученные из социальной сети (делая это на автомате, вы рискуете привести пользователя в ярость, переписывая данные в его профиле), в системе есть специальный сигнал pre_update. Его вы можете обработать и ответить, нужно ли вносить изменения.

При успешной авторизации в специальной таблице сохраняется информация о том, какой пользователь связан с какими (1:N) сервисами авторизации. Для привязки уже залогиненного пользователя используются методы библиотеки associate.

«Просто, понятно, легко запомнить», как говорил капитан Д. Воробей.

Что брать и на что обратить внимание

Итак, мой форк здесь: https://github.com/krvss/django-social-auth

В существующую архитектуру довольно легко вписался ВКонтакте – его backend сейчас, кроме авторизации, проверяет также «подпись» данных, чтобы чего не вышло.

Когда вы, авторизуясь на Yandex через OpenId, ставите галочку «передать дополнительные параметры», в Django почему-то возникает нарушение целостности или потеря csrf-информации. Чтобы не возникала ошибка, можно указать свою функцию для завершения авторизации (параметр SOCIAL_AUTH_COMPLETE_URL_NAME), которая будет отличаться только наличием декоратора csrf_exempt_view и состоять из вызова social_auth.complete.

Upd: Причины такого поведения Yandex объясняются в комментариях к этой записи.