Django Social Auth: авторизация через pop-up

30 декабря 2012

Меня часто спрашивают, как сделать авторизацию с использованием pop-up так, чтобы пользователю не нужно было покидать главную страницу приложения.
Я сделал пример в своей ветке, но также оформил pull request в основную ветку.

Всех с наступающими праздниками!

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 🙂

United DSA

10 мая 2012

Функциональность моего форка была перенесена в основную ветку Django Social Auth. Танцуем.

Благодарности: bacher09 и gugu.

ВНИМАНИЕ: изменились имена настроек для Yandex и VKontakte. Новые имена: YANDEX_APP_ID, YANDEX_API_SECRET; VK_APP_ID, VK_API_SECRET.

С меня: код примеров для авторизации на VK и FB приложениях и для загрузки картинок. А, и ассоциацию с Яндексом неплохо бы решить.

Upd 13.07.2012: Пример для Facebook добавлен.
Upd 23.08.2012: Пример для ВКонтакте добавлен.

Django Social Auth: Авторизация на Яндекс через OAuth2

4 января 2012

Понедельник, как говорится, начинается в субботу — праздники дали возможность немного поработать над своими проектами. Добавлен backend для авторизации на Яндекс через OAuth2. Особенности/ограничения:

  • Нельзя указывать права доступа в настройках приложения. Права доступа задаются для разных сервисов Яндекса при регистрации приложения. Там же прописывается redirect_uri.
  • В настройке YANDEX_OAUTH2_API_URL указывается URL того сервиса, из которого вы хотите получать информацию о пользователе. Проверено и работают 2 сервиса: Я.ru, url=’https://api-yaru.yandex.ru/me/’ и Мой Круг, url=’http://api.moikrug.ru/v1/my/’. Не получилось проверить Яндекс.Фотки (url=’http://api-fotki.yandex.ru/api/me/’), как я не пытался все время возникала 500 Internal Server Error. Возможно, в праздники идет какой-то апгрейт.

Буду благодарен за обратную связь по работе backend’a.

Обновление от 19.01
Служба поддержки Яндекса помогла разобраться с проблемой вызова API Фоток, за что я ей очень благодарен. Я переделал backend чтобы этот API можно было использовать в будущем, на сегодняшний день его использовать пока нельзя. Причина в том, что ответ от me не содержит id пользователя в системе (совсем) и login пользователя (неявно он есть в url альбомов), писать специально для Фоток частный случай обработки не очень хочется, надеюсь это исправится со временем.

Обновление 2
Код получения аватаров пользователей обновлен для поддержки Яндекса.

Новые функции Django Social Auth

20 декабря 2011

В комментариях к этой записи оставляйте, пожалуйста, ваши пожелания по новой функциональности Django Social Auth.
В ближайших планах: поддержка OAuth для Яндекса (ожидается после новогодних праздников).

Всех с наступающим Новым годом!

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

18 октября 2011

Меня часто спрашивают, а я ленюсь отвечать одно и то же. Поэтому хочу здесь дать несколько практических советов тем, кто начинает разбираться с DSA.

1. С чего начать?
Начинайте всегда с примера django-social-auth/example, а в нем — с простых авторизаций вроде OpenId. Потом можно заполнить local_settings и переходить к OAuth, но имейте в виду, что многие из них потребуют запуска на доменном имени вашего сайта, а не на localhost.

2. Не работает!
Когда я делаю merge больших изменений из основной ветки, то всегда проверяю, не сломалась ли авторизация (бывает, что ломается), и только потом выкладываю код на github. Это я как бы намекаю, что проблема, скорее всего, на вашей стороне. Проверьте настройки, URL’ы и импорты.

3. Используется ли в реальных проектах?
Да, в частности на http://conceptor.ru. Собственно, для этого проекта все и разрабатывалось, сайт можно использовать в качестве развернутого примера.

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.

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

← Предыдущие записи