Djangoでスマホアプリ向けにソーシャル連携APIを作る


# 背景

自前のサービスのアカウントを、OAuthを利用してfacebookやTwitterなどの外部サービスのアカウントと連携させたいときがある。
Python3向けにおいては、[python-social-auth](https://github.com/omab/python-social-auth)を使えばWebサービス用の外部アカウント連携画面は簡単に追加できる。ただスマホアプリで同じことがしたい場合はAPIでの連携機能を提供する必要があるので、対応方法についてのメモ。

※最初は[django-social-auth](https://github.com/omab/django-social-auth)の利用を検討したが、Python3系では動かないらしい。

# 目的

今回はスマホアプリでのユーザー引き継ぎ機能を想定。
利用したい外部サービスはfacebookとTwitter。
実装する機能は以下の2点。

1. 既に存在しているDjangoユーザーに対して、外部アカウントを紐付けるAPI
2. 外部アカウントの情報から、紐付いているDjangoユーザー情報を取得するAPI

※ちなみに外部アカウントを元に新規ユーザーを作成する処理は以下のページの情報がほぼそのまま利用できると思う。

[Social Auth with Django REST Framework]
(https://yeti.co/blog/social-auth-with-django-rest-framework/)

# 環境

Python 3.4.3
Django 1.8
- djangorestframework 3.2.3
- python-social-auth 0.2.13

# 認証の流れ

認証処理の流れとしては、OAuthを利用した以下のような利用を想定している。

1. スマホアプリ内で各種SDKを利用するなどして、認証用のOAuthトークンを取得
	* ユーザーと外部サービス間の通信
2. スマホアプリから自前サービスに対して、1.で取得したOAuthトークンを送信
	* ユーザーと自前サービス間の通信
3. 自前サービスで受け取ったOAuthトークンを利用して、外部サービスからユーザーIDを取得
	* 自前サービスと外部サービス間の通信
4. 3.でユーザーIDが取得できた場合は、必要な処理(ユーザー情報のひも付け等)を行い、処理の結果をスマホアプリに返す
	* 自前サービスとユーザー間の通信

# 実装内容

python-social-authのインストール

```sh:
$ pip install python-social-auth
```

python-social-auth向けの設定
※Twitterやfacebookアプリの情報は適宜書き換え

```settings.py
INSTALLED_APPS = (
    ...
    'social.apps.django_app.default',
    ...
)

TEMPLATE_CONTEXT_PROCESSORS = (
    ...
    'social.apps.django_app.context_processors.backends',
    'social.apps.django_app.context_processors.login_redirect',
    ...
)

AUTHENTICATION_BACKENDS = (
   'social.backends.facebook.FacebookOAuth2',
   'social.backends.twitter.TwitterOAuth',
   'django.contrib.auth.backends.ModelBackend',
)

SOCIAL_AUTH_PIPELINE = (
    'social.pipeline.social_auth.social_details',
    'social.pipeline.social_auth.social_uid',
    'social.pipeline.social_auth.auth_allowed',
    'social.pipeline.social_auth.social_user',
    # 認証情報チェック時にユーザーが作成されないように外しておく
    #'social.pipeline.user.create_user',
    'social.pipeline.social_auth.associate_user',
    'social.pipeline.social_auth.load_extra_data',
    'social.pipeline.user.user_details',
)

SOCIAL_AUTH_TWITTER_KEY = "MY_TWITTER_APP_KEY "
SOCIAL_AUTH_TWITTER_SECRET = "MY_TWITTER_APP_SECRET"

SOCIAL_AUTH_FACEBOOK_KEY = "MY_FACEBOOK_APP_ID"
SOCIAL_AUTH_FACEBOOK_SECRET = "MY_FACEBOOK_APP_SECRET"
```

認証情報用テーブルの作成

```sh:
$ ./manage.py migrate
```

プロジェクト全体のurl設定

```myproject/urls.py
from django.conf.urls import url

urlpatterns = [
    ...
    url('', include('social.apps.django_app.urls', namespace='social'))
    ...
]
```

連携機能を利用したいappのurl設定

```myapp/urls.py
from django.conf.urls import url
from . import views

urlpatterns = [
	# 1. 既に存在しているユーザーに対して、外部アカウントを紐付けるAPI
    url(r'api/associate/(?P<backend>[^/]+)/$', views.associate_account, name='associate_account'),
	# 2. 外部アカウントの情報から、紐付いているユーザー情報を取得するAPI
    url(r'api/auth/(?P<backend>[^/]+)/$', views.auth_account, name='auth_account'),
]
```

連携機能を利用したいappのview

```myapp/views.py
from django.contrib.auth import login

from rest_framework import status
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework.decorators import api_view, permission_classes

from social.backends.oauth import BaseOAuth1, BaseOAuth2
from social.apps.django_app.utils import psa

# 1. 既に存在しているユーザーに対して、外部アカウントを紐付けるAPI

@api_view(('POST',))
@permission_classes((IsAuthenticated,))
@psa('social:complete')
def associate_account(request, backend):
    backend = request.backend
    token = _make_token(request, backend)
    # OAuthでの認証に成功した場合のみuserが返ってくる
    user = backend.do_auth(token, user=request.user)
    if user:
        login(request, user)
        return Response({'success': True})
    else:
        return Response({"errors": "Error with social authentication"},
                            status=status.HTTP_400_BAD_REQUEST)

# 2. 外部アカウントの情報から、紐付いているユーザー情報を取得するAPI

@api_view(('POST',))
@psa('social:complete')
def auth_account(request, backend):
    backend = request.backend
    token = _make_token(request, backend)
    # OAuthでの認証に成功し、その情報に紐付いたユーザーが存在する場合のみuserが返ってくる
    user = backend.do_auth(token)
    if user:
        return Response({'id': user.id, 'username': user.username})
    else:
        return Response({"errors": "User Not Found"},
                            status=status.HTTP_404_NOT_FOUND)

# OAuth1とOAuth2では利用するトークンの形が異なる

def _make_token(request, backend):
    if isinstance(backend, BaseOAuth1):
        token = {
            'oauth_token': request.data.get('access_token'),
            'oauth_token_secret': request.data.get('access_token_secret'),
        }
    elif isinstance(backend, BaseOAuth2):
        token = request.data.get('access_token')
    return token
```

# 動作確認

## Twitterとの連携

### 準備

1. 自前のTwitterアプリと連携したユーザー用のaccess_tokenとaccess_token_secretを(なんらかの方法で)用意する。
2. ブラウザで自前サービスの管理画面にログインしておく。

### 外部サービス連携機能の確認方法

associate_accountを設定したURL(ex. http://myserver/myapp/api/associate/twitter/ )にブラウザでアクセス。

![Django_REST_framework.png](https://qiita-image-store.s3.amazonaws.com/0/62874/467d8bc1-42a6-c7d7-64ef-e0b30cdc2702.png "Django_REST_framework.png")

上の画面のように、contentにjson形式でaccess_tokenとaccess_token_secretを記入し、[POST]を押す。

```contentの中身
{
   "access_token": "my_access_token",
   "access_token_secret": "my_access_token_secret"
}
```

`{ "success": true }`が返ってきたら認証成功。

![Django_REST_framework.png](https://qiita-image-store.s3.amazonaws.com/0/62874/890abe5c-582c-7397-3de8-f5a23c3e54ae.png "Django_REST_framework.png")

### 外部サービス認証機能の確認方法

auth_accountを設定したURL(ex. http://myserver/myapp/api/auth/twitter/ )にブラウザでアクセスし、前項と同じ手順を行う。

先ほど連携を行ったユーザーのIDが返ってくれば成功。

![Django_REST_framework.png](https://qiita-image-store.s3.amazonaws.com/0/62874/d0fa95b2-10fd-988d-c4a5-957a054cd004.png "Django_REST_framework.png")

## Facebookの場合

Twitterのとほぼ同じ手順で確認できるが、以下の2点だけ異なる。
1. 上記の確認手順で、`twitter`となっているところを`facebook`にする。
2. 送信するjsonの情報は、`access_token`のみでよい。(OAuth2は`access_token_secret`が存在しない)

# 感想

[python-social-auth](https://github.com/omab/python-social-auth)便利！
認証に必要なコードが数行で書けた！

ただ今回の実装は散らばっている英語の情報をいろいろかき集めてやっと実現できた感じで、改めてPythonはまとまった日本語の情報少ないなーという印象を持った。

# 参考

Python 3.4とDjango 1.6.5でTwitterやFacebookでのログインをしたい - 今日のハック
http://narusemotoki.tumblr.com/post/90525892180/python-34%E3%81%A8django-165%E3%81%A7twitter%E3%82%84facebook%E3%81%A7%E3%81%AE%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E3%82%92%E3%81%97%E3%81%9F%E3%81%84

omab/python-social-auth
https://github.com/omab/python-social-auth

Django Framework — Python Social Auth documentation
https://python-social-auth.readthedocs.org/en/latest/configuration/django.html

Pipeline — Python Social Auth documentation
https://python-social-auth.readthedocs.org/en/latest/pipeline.html#authentication-pipeline

Use Cases — Python Social Auth documentation
http://psa.matiasaguirre.net/docs/use_cases.html#signup-by-oauth-access-token

Twitter OAuth using access_token · Issue #272 · omab/python-social-auth
https://github.com/omab/python-social-auth/issues/272

Social Auth with Django REST Framework - Yeti
https://yeti.co/blog/social-auth-with-django-rest-framework/