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 🙂

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 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.

Remember Me на Django

22 февраля 2011

Недавно возникла необходимость сделать простую вещь – функциональность «запомнить меня». Требования простые: при установленном флажке «запомнить» пользователь считается прошедшим авторизацию и не должен вводить пароль еще раз. Выход из системы в этом случае производится при нажатии кнопки/ссылки «выйти».

При попытке найти что-то готовое результат был курьезный: кто-то решает это путем установки времени истечения сессии в 2036–2039 годах, другие примеры просто неправильные (по всей видимости, написаны под старые версии Django). Пришлось писать решение самому.

Если ваш сайт использует сессии, то механизм «запомнить меня» довольно простой: после успешного прохождения авторизации нужно, используя метод сессии set_expiry, указать, до какого момента будет действительна данная сессия. К сожалению (или к счастью), бесконечно она действительна быть не может, поэтому вы должны либо ее обновлять время от времени, либо просто поставить срок истечения, равный времени, когда вы планируете сменить работу, плюс еще пару лет, чтобы уж наверняка это стало чужой проблемой.

Я решил задачу, написав очень простой и понятный middleware:

from django.conf import settings
from datetime import timedelta, date
class KeepLoggedInMiddleware(object):
    def process_request(self, request):
        if not request.user.is_authenticated() or not settings.KEEP_LOGGED_KEY in request.session:
            return
        if request.session[settings.KEEP_LOGGED_KEY] != date.today():
            request.session.set_expiry(timedelta(days=settings.KEEP_LOGGED_DURATION))
            request.session[settings.KEEP_LOGGED_KEY] = date.today()
        return

Чем удобен middleware: не требуется никакой дополнительный код где-то еще. Для работы в Settings нужно сделать следующее:

  1. Добавить имя_модуля. KeepLoggedInMiddleware в MIDDLEWARE_CLASSES (я поставил его последним, чтобы уже работал middleware сессий)
  2. Добавить настройки:
# Keep me logged settings
KEEP_LOGGED_KEY = 'keep_me_logged'
KEEP_LOGGED_DURATION = 365 # in days

KEEP_LOGGED_KEY – имя ключа с данными в сессии пользователя, KEEP_LOGGED_DURATION – срок в днях, который вы бы хотели помнить пользователя. Одного года, на мой взгляд, более чем достаточно.
Затем в функции обработки формы авторизации, если флажок «запомнить меня» включен, вы делаете примерно следующее:

   form = LoginForm(request.POST)
    if form.is_valid():

            ....

            if form.remember:
                request.session[settings.KEEP_LOGGED_KEY] = True

После чего при обработке любого запроса middleware увидит, что настройка включена, а обновление не случилось, и обновит дату в сессии. Чаще чем раз в день, на мой взгляд, этого делать не нужно.

Можно не хранить дату последнего обновления, а просто проверять, когда истечет текущая сессия, и продлевать ее – правда, количество кода при этом не изменится, так что это дело вкуса.

При выходе из системы (logout) Django автоматически почистит все данные сессии, т. е. никаких дополнительных действий не требуется.

Remember Me by SylarNight