Django の Twitter 認証で悩んだところ

<dl>
<dt>2020-02-02 更新</dt>
<dd>"Twitter でログイン" ボタンのリンク先に next パラメタを付けました。</dd>
</dl>

# 前置き

### social-auth-app-django を使う

Django で Web アプリを作っていて、ログインに Twitter 認証を使うことにしました。
[social-auth-app-django](https://github.com/python-social-auth/social-app-django) を使うと簡単にできると聞いて、使うことにしました。

social-auth-app-django の使い方については、[Python Social Auth’s documentation](https://python-social-auth.readthedocs.io/en/latest/configuration/django.html) という公式のドキュメントがあります。
他にも、ネットで検索すれば簡単に使い始める方法がいくつかヒットします ([これ](https://qiita.com/gbA-3/items/b9dc1de5d8e00163f49a)とか)。

### この記事で扱う内容

social-auth-app-django はとても簡単に導入できるようになっているのですが、それでも導入する上で悩んだ箇所があったので、この記事では私が悩んだ部分について書きます。

※ social-auth-app-jango は Twitter 以外にも対応していますが、この記事では Twitter 認証のみ扱います。

# その前に全体の手順を確認する

唐突に悩んだ箇所の話をするのも分かりにくいと思うので、ざっと導入の手順を確認しておきます。
細かい説明はしません。分かっている方は飛ばしてください。

### 1. Twitter に API アクセス用のアプリを作る

Twitter にデベロッパ登録していることが前提です。
アプリを作るのは、[ダッシュボード](https://developer.twitter.com/en/apps) から "Create an app" を押して、必要事項を記入するだけです。

私の場合ここで、Twitter アプリが作れないという問題が発生しました。
また、Callback URLs は required (必須) ではありませんが、アプリを認証に使うには正しく設定する必要があります。
詳しくは下の方の [**"悩んだところ"**](#悩んだところ) で説明します。

Twitter アプリが出来て、Callback URLs も正しく設定されたものとして、次へ進みます。

### 2. モジュールのインストール

pip なり pipenv で、PyPI からインストールできます。

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

### 3. API key と API secret key

Twitter API にアクセスするのに、API key と API secret key が必要になります。
確認するには [ダッシュボード](https://developer.twitter.com/en/apps) から作ったアプリの Details へ進み、Keys and tokens というタブを選択します。
表示された Consumer API keys の2つのキーをコピーして使います。

これらのキーは公開してはいけませんが、開発用であれば settings.py に直接書いても良いでしょう。

```python:settings.py
SOCIAL_AUTH_TWITTER_KEY = 'XXXXXXXXXXXXXXX'
SOCIAL_AUTH_TWITTER_SECRET = 'YYYYYYYYYYYYYY'
```

プロジェクトを公開している場合はリポジトリに含まない形で、ローカルとそれ以外でそれぞれ読み込めるようにします。
`SECRET_KEY` 変数で同じことをしているはずなので問題ないでしょう。

### 4. settings.py の編集

`INSTALLED_APPS` に 'social_django' を追加します。

```python:settings.py
INSTALLED_APPS = [
    ...
    'social_django',    # 追加
]
```

`TEMPLATES` に context_processors を追加します。

```python:settings.py
TEMPLATES = [
    {
        ...
        'OPTIONS': {
            'context_processors': [
                'social_django.context_processors.backends',        # 追加
                'social_django.context_processors.login_redirect',  # 追加
            ],
        },
    },
]
```

Name space と Backends を追加します。

```python:settings.py
# For social-auth-app-django
SOCIAL_AUTH_URL_NAMESPACE = 'social'                # 追加
AUTHENTICATION_BACKENDS = [
    'social_core.backends.twitter.TwitterOAuth',    # 追加
    'django.contrib.auth.backends.ModelBackend',    # 追加
]
```

### 5. urls.py の編集

`urlspatterns` に 'social_django.urls' をインクルードします。

```python:urls.py
urlpatterns = [
    ...
    path('', include('social_django.urls')),        # 追加
    path('admin/', admin.site.urls),
    ...
]
```

これにより `/login/twitter/`, `/complete/twitter/`, `/disconnect/twitter/` というリクエストに反応できるようになります。

ちなみにドキュメントでは以下のような (古い) 書き方になっています。

```python
    url('', include('social_django.urls', namespace='social')),
```

### 6. migrate

ここまで設定できたら、migrate して DB を更新します。
Twitter アカウントと Django のアカウントを関連付けるテーブルが追加されます。

```
$ python manage.py makemigrations social_django
$ python manage.py migrate
```

### 7. ログインページを作る

ネットで social-auth-app-jango の使い方を紹介している記事をみると、ログインページを1から作っているページと、ログインボタンの書き方だけを説明しているページがありました。

どうするのが良いのか、ちょっとだけ悩んだので以下で説明します。

ちなみにボタンの書き方はこうです。

```html
<button type="button" class="button"
    onclick="location.href='{% url 'social:begin' 'twitter' %}?next={{ next }}'">
    Twitter でログイン
</button>
```

これを設置すれば、Twitter 認証ができるようになります。

# 悩んだところ

上記の手順の中で、私が悩んだところについて説明します。

### Twitter アプリが作れない

もしかしたら、最近 Twitter のデベロッパ登録をした人は、この問題は起きないかも知れません。
私は以前からデベロッパ登録をしていて、Django のためにアプリを作ろうとしたところ、再度デベロッパ申請をするように言われました。

指示どおり申請をすると、以下のページが表示されました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/3ff694c5-9037-da9e-90d1-e0733ddfb061.jpeg" height="300" />

これでアプリを作れるはず、と思ったのですが、必要事項を記入して注意事項も読んで、これで終わりかと思ったところでこんな画面になりました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/695a1688-0efd-5c85-8f69-b9018ea6cf0b.jpeg" height="300" />

#### 原因

[このページ](https://teratail.com/questions/174553) を参考に解決することができました。

結論としては私の場合、Twitter アカウントに電話番号を設定したらアプリを作れるようになりました。
電話番号をどこから設定するのかも最初わからなかったので書いておきますと、デベロッパのダッシュボードではなく、普通に Twitter のホームから、「設定とプライバシー」>「アカウント」で設定できました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/a486e21b-0b0b-3502-b6ba-9c1b19e0f7ca.png" height="300" />

### Callback URLs

「Twitter でログイン」ボタンを設置して最初に動作確認した時、403エラーになりました。
原因は Twitter アプリ側の Callback URLs の設定でした。

前置きのところで参照した [こちらのページ](https://qiita.com/gbA-3/items/b9dc1de5d8e00163f49a) にはちゃんと書いてあったのですが、最初は他のページを見ていたせいで、Callback URLs が間違っていました。
設定によっても変わりますので、自分の環境で urls.py を確認するのが良いでしょう。

この記事の上記の手順で設定した場合は、以下の URL を設定するのが正しいようです。

- ローカルで runserver する場合
    - http\://127.0.0.1:8000/complete/twitter/
- Web にデプロイする場合
    - サービスのルート/complete/twitter/

### ログインページをどうするか

最初に参照したページではログイン/ログアウトページを1から作り、そのためのアプリ (Django プロジェクト内の app) も作っていました。
そうする必要があるのかと思いましたが、そうではありませんでした。
上の **"7. ログインページを作る"** で書いたボタンを、すでにあるログインページに貼るだけです。

もちろん、Twitter 認証でログインするための特別なページを作りたい場合はそうすれば良いです。
私のプロジェクトではサインアップ機能もつけていないので、ログインは Twitter 認証だけにして、元々あったフォームはログインページのテンプレートから削除 (コメントアウト) してしまいました。

#### テンプレートの書き換え

ログイン/ログアウトページのテンプレートは、たいてい 'templates/registration/' フォルダに入っていると思います。
実際にはプロジェクトによって異なりますので、そのプロジェクトで使っているテンプレートを見つけて編集します。

こんな感じで、元々あったフォームを単品のボタンに置き換えてしまいました。

```html:login.html
    ...
    <div align="center">
        <button type="button" class="button"
        onclick="location.href='{% url 'social:begin' 'twitter' %}?next={{ next }}'">
        Twitter でログイン</button>
    </div>
    
    <!--
    <div align="center">
        <form method="post" action="{% url 'login' %}">
            {% csrf_token %}
            <table>
                {{ form.as_table }}
            </table>
            <div>&nbsp;</div>
            <input type="submit" class="button" value="Login"/>
            <input type="hidden" name="next" value="{{ next }}"/>
        </form>
    </div>
    -->
    ...
```

表示されたログインページ
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/c77f1f73-72cf-c1b3-6eb2-583ad117940c.png" height="150" />
元々あったフォームをコメントアウトしなければ、「Twitter でログイン」ボタンと共存させることもできます。
ログアウトページは変更の必要はありません。

#### admin でログインするには

上の例ではパスワードでのログインは出来ないようにしましたが、admin でログインしたい場合は 'サービスのルート/admin/' にアクセスすれば、admin 用のログインページが開きます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/f6b8a0cd-644e-4bae-da05-ad4e44ce6526.png" height="220" />

ただしログアウトページについては、たいてい共通のものを使っているので、admin でログアウトして「もう一度ログインする」というリンクを押しても「Twitter でログイン」の方へ行ってしまうと思います。
管理用のページにはアクセスし難い方が良いので、私はそのままにしています。