DjangoでTwitter認証アプリケーションを作成する[2018/06最新版]

# はじめに
PythonのwebフレームワークであるDjangoでTwitter認証がしたい．

いくつかのサイトを参考に試みるも**403エラー**が発生し，なかなかうまく行きませんでした．

**結論からいうと，TwitterがOAuth認証時にCallback URLをチェックするようになったことが原因．**

今回はその点を考慮した，DjangoでのTwitter認証実装手順を紹介します．


**この手順通りに実行することで，DjangoでTwitter認証を用いたアプリケーションを作成できます．**
# 手順
## 開発環境
- Python 3.5.3
- Django 2.0.6
- social-auth-app-django 2.1.0

## Djangoと認証ライブラリのインストール 
Djangoと[social-app-django](https://github.com/python-social-auth/social-app-django)のインストールを行います．
social-app-djangoは，Djangoで様々なwebサービスでの認証を実装するためのライブラリです．

```sh
$pip install django social-auth-app-django
```

## プロジェクトとアプリケーションの作成
プロジェクトの作成を行います．

```sh
$django-admin startproject project
```

次にアプリケーションの作成を行います．
今回は**user_auth**という名前で作成します．

```sh
$cd project/
$python manage.py startapp user_auth 
```

この時点では以下のようなディレクトリ構造になります．

```sh
.
├── manage.py
├── project
│   ├── __init__.py
│   ├── __pycache__
│   │   ├── __init__.cpython-35.pyc
│   │   └── settings.cpython-35.pyc
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── user_auth
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── migrations
    │   └── __init__.py
    ├── models.py
    ├── tests.py
    └── views.py
```

## マイグレーション実行
migrateを行います．

```sh
$python manage.py migrate
```

正常にマイグレーションが実行されると，以下のような表示になります．
<img width="570" alt="スクリーンショット 2018-06-24 21.05.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/133979/953be581-7d5d-79ab-e339-31bdac595667.png">

※この時点で，サーバを起動し，

```sh
$python manage.py runserver
```

[localhost:8000](localhost:8000)にアクセスし，アプリケーションが正しく実行できるか確認しても良いでしょう．

## Twitterアプリケーションの作成
Twitter認証を行うにあたり，

- Counsumer Key
- Consumer Secret
 
が必要となります．
[Twitter Application Mangement](https://apps.twitter.com/)にアクセスし，Twitterアプリケーションの作成を行い，キーを入手します．

上記リンクにアクセス後，Create New　Appをクリックし，以下の画面に行きます．

**Name，Description，Website**を埋め，**Developer Agreement**にチェックを入れた後，下部の**Create your Twitter application**をクリックし，作成します．

(実は**Callback URL**が後にとても重要になるのですが，とりあえず今は空白にしておきます．)

![スクリーンショット 2018-06-24 21.20.07.png](https://qiita-image-store.s3.amazonaws.com/0/133979/84e93245-05ed-8f80-48b0-626c57928b95.png)

そして，作成したTwitterアプリケーションのKeys and Access Tokensタブに移動し，

- Consumer Key (API Key)
- Consumer Secret (API Secret)

をひかえておきます．

![スクリーンショット 2018-06-24 23.25.44.png](https://qiita-image-store.s3.amazonaws.com/0/133979/549d67e8-fee4-2bc7-db2f-bc89fe39f3ed.png)


## 設定ファイルの書き換え
project/settings.pyの書き換えを行います．
先程取得した，TwitterアプリケーションのKeyを入力します．


```diff:project/settings.py

...
# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
+    'user_auth',
+    'social_django',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'project.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
+                'social_django.context_processors.backends',
+                'social_django.context_processors.login_redirect',
            ],
        },
    },
]

WSGI_APPLICATION = 'project.wsgi.application'


...


# Internationalization
# https://docs.djangoproject.com/en/2.0/topics/i18n/

- LANGUAGE_CODE = 'en-us'
+ LANGUAGE_CODE = 'ja'

- TIME_ZONE = 'UTC'
+ TIME_ZONE = 'Asia/Tokyo'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.0/howto/static-files/

STATIC_URL = '/static/'

+ AUTHENTICATION_BACKENDS = [
+     'social_core.backends.twitter.TwitterOAuth',
+     'django.contrib.auth.backends.ModelBackend',
+ ]

+ SOCIAL_AUTH_TWITTER_KEY = 'xxxxxxxxxxx' # Consumer Key (API Key)
+ SOCIAL_AUTH_TWITTER_SECRET = 'xxxxxxxxxxx'　# Consumer Secret (API Secret)
+ SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/user/top' # リダイレクトURL
```

## マイグレーション実行(2回目)
INSTALLED_APPSにsocial_djangoを加えたため，もう一度，マイグレーションを実行します．

```shell
$python manage.py migrate
```
<img width="569" alt="スクリーンショット 2018-06-25 0.01.14.png" src="https://qiita-image-store.s3.amazonaws.com/0/133979/5304c81a-be57-8f17-d4fb-52588695492c.png">


## ルートの書き換え


```diff:project/urls.py
from django.contrib import admin
- from django.urls import path
+ from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
+   path('user/', include('user_auth.urls')),
+   path('user/', include('social_django.urls', namespace='social')),
]
```

user_auth直下に**urls.py**を作成し，以下を記述します．

```python:user_auth/urls.py
import django
import django.contrib.auth.views
from django.urls import path,include
from . import views
app_name='user_auth'

urlpatterns=[
    path('top/',views.top_page, name="top"), # リダイレクト
    path('login/', # ログイン
     django.contrib.auth.views.LoginView.as_view(template_name = 'user_auth/login.html'),
     name='login'),
    path('logout/', # ログアウト
     django.contrib.auth.views.LogoutView.as_view(template_name = 'user_auth/login.html'),
     name='logout'),
]
```

## テンプレートの作成
**user_auth/templates/user_auth**ディレクトリを作成．
その中に，ログイン・ログアウト・リダイレクトの各テンプレートを作成．

```html:user_auth/templates/user_auth/login.html
<html>
<head>
    <title>ログイン</title>
</head>
<body>
<div>
    <button type="button" onclick="location.href='{% url 'social:begin' 'twitter' %}'">Twitterログイン</button>
</div>
</body>
</html>

```

```html:user_auth/templates/user_auth/logout.html
<html>
<head>
    <title>ログアウト</title>
</head>
<body>
<div>
    <p>
        ログアウトしました．
    </p>
    <p>
        <a href="/user/login"><button type="button" >ログインページへ</button></a>
    </p>
</div>
</body>
</html>
```

```html:user_auth/templates/user_auth/top.html
<html>
<head>
    <title>TOP</title>
</head>
<body>
    <div>
        <a href="/twitterManager/logout"><button type="button" >ログアウト</button></a>
    </div>
<div>
    <div>
      <p><b>ScreenName</b>:{{ user.access_token.screen_name }}</p>
        <p><b>UserId</b>:{{ user.access_token.user_id }}</p>
        <p><b>OAuthTokenSecret</b>:{{ user.access_token.oauth_token_secret }}</p>
        <p><b>OAuthToken</b>:{{ user.access_token.oauth_token }}</p>
        <p><b>AuthTime</b>:{{ user.extra_data.auth_time }}</p>


    </div>
</div>
</body>
</html>


```


## ビューの作成

リダイレクト用のビューを作成します．

```user_auth/views.py
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from social_django.models import UserSocialAuth


@login_required
def top_page(request):
    user = UserSocialAuth.objects.get(user_id=request.user.id)

    return render(request,'user_auth/top.html',{'user': user})

```

# 全体のディレクトリ構造
最終的に以下のような構造となります．

```bash
.
├── db.sqlite3
├── manage.py
├── project
│   ├── __init__.py
│   ├── __pycache__
│   │   ├── __init__.cpython-35.pyc
│   │   ├── settings.cpython-35.pyc
│   │   ├── urls.cpython-35.pyc
│   │   └── wsgi.cpython-35.pyc
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── user_auth
    ├── __init__.py
    ├── __pycache__
    │   ├── __init__.cpython-35.pyc
    │   ├── admin.cpython-35.pyc
    │   ├── models.cpython-35.pyc
    │   ├── urls.cpython-35.pyc
    │   └── views.cpython-35.pyc
    ├── admin.py
    ├── apps.py
    ├── migrations
    │   ├── __init__.py
    │   └── __pycache__
    │       └── __init__.cpython-35.pyc
    ├── models.py
    ├── templates
    │   └── user_auth
    │       ├── login.html
    │       ├── logout.html
    │       └── top.html
    ├── tests.py
    ├── urls.py
    └── views.py
```

# サーバ起動
サーバを起動し，実際にアクセスしてみます．

```sh
$python manage.py runserver
```

[localhost:8000/user/login](localhost:8000/user/login)
にアクセス．

<img width="545" alt="スクリーンショット 2018-06-25 13.25.25.png" src="https://qiita-image-store.s3.amazonaws.com/0/133979/26d6dbb1-bc59-08d4-00a9-9e3a215adf12.png">

#　重要ポイント
さあ，いざログイン．


ログインボタンをおすと，**401エラー**が発生します．
これは先程のTwitterアプリケーションの設定で，Callback URLを空白にしたため．
![スクリーンショット 2018-06-25 0.38.48.png](https://qiita-image-store.s3.amazonaws.com/0/133979/c4a61a68-3b61-6fa5-549e-94e62a67bae1.png)


以前までは，とりあえず適当なURLを入力しておくことで，この問題は解決できました．

しかし，2018年6月からはここに適当なURLを入力すると，**403エラー**が発生します．
<img width="954" alt="スクリーンショット 2018-06-25 13.29.29.png" src="https://qiita-image-store.s3.amazonaws.com/0/133979/eeb83a57-f399-39ab-a393-914a4215602e.png">


なにを入力すればいいのか．


ずばり，
**`http://localhost:8000/user/complete/twitter/`**
です．
<img width="1071" alt="スクリーンショット 2018-06-25 13.21.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/133979/15d84779-a1f7-2727-f1a1-907397b95cf7.png">
(最後の`/`を忘れず！）

今回はuset_authをTwitter認証のアプリケーションとしており，そのパスを**user/**としているため，このようなCallback URLとなります．

Callback URLをこのように変更し，ログインボタンを押すと，
<img width="692" alt="スクリーンショット 2018-06-25 13.35.23.png" src="https://qiita-image-store.s3.amazonaws.com/0/133979/2d8529de-2c99-f042-fde2-6ea51952846e.png">
このようにログインが可能となります．

このあと，自動的にトップページへとリダイレクトされます．
<img width="856" alt="スクリーンショット 2018-06-25 13.43.56.png" src="https://qiita-image-store.s3.amazonaws.com/0/133979/4a746075-613a-6d66-e2d7-7e48a9f4c2b8.png">

# 終わりに
以上が2018/06の仕様変更に対応した，DjangoでのTwitter認証実装となります．

あとは，標準のユーザ認証と同じように，ログイン情報などのチェックを行うことが可能となります．


# 参照サイト
[[プロ生]Twitter が OAuth 認証時 Callback URL をチェックするようになったみたい](http://pronama.azurewebsites.net/2018/06/13/twitter-oauth-callback-url/)
[[Qiita]PythonのWebアプリフレームワーク「Django」を使ってTwitter認証してみた](https://qiita.com/bakira/items/175b16e7e5372e007920#_reference-347b3631a015aaaf6156)
[[Github]Twitter login not working](https://github.com/python-social-auth/social-app-django/issues/164)