[入門]Twitter API の OAuth2.0 ユーザー認証をライブラリに頼らずやる

さすがにHttpリクエストをするためのライブラリは使いますよ。

# お断り

筆者はOAuthなんてTwitterAPIで初めて知ったような認証認可初学者なので、情報には誤りがあるかもしれません。

誤った点があれば遠慮なく教えてください。

# ステップ

公式ドキュメントのステップ番号とは対応していないので注意

https://developer.twitter.com/en/docs/authentication/oauth-2-0/user-access-token

1. 🚶‍♂**デベロッパーポータルからOAuth2.0を有効にし、`Client ID` と `Client Secret` をメモする。**
1. 🚵**アカウントの持ち主に開かせるURLを生成する**
    - `Client ID`
    - `redirect_uri` (OAuth2.0有効化時に登録したもののみ可能。)
    - `Scopes` [細かく分けられた権限。](https://developer.twitter.com/en/docs/authentication/oauth-2-0/authorization-code#:~:text=Scopes%20allow%20you%20to%20set%20granular%20access%20for%20your%20App%20so%20that%20your%20App%20only%20has%20the%20permissions%20that%20it%20needs.%20To%20learn%20more%20about%20what%20scopes%20map%20to%20what%20endpoints,%20view%20our%20authentication%20mapping%20guide.)各APIエンドポイントによって必要なスコープがある
    - `code_challenge` 十分にランダムな文字列。
1. 🚗**URLを踏んだ先(Twitterのページ)でアカウントの持ち主に権限のリクエストを承認してもらい、リダイレクトURLのクエリから `code` を拾う**
1. 🚅**今までの情報を使ってアクセストークンを要求する**
    - `code` リダイレクトurlのコード
    - `redirect_uri` (ステップ2と同じもの)
    - `code_verifier` (ステップ2の`code_challenge`で使用した文字列)
    - `Client ID`と`Client Secret` Basic Authorizationに使用
1. 🚀**Scope `offline.access` が有効の場合、トークンをリフレッシュできる。**
    - `refresh_token` アクセストークンと一緒に取得したもの
    - `Client ID`と`Client Secret` Basic Authorizationに使用

## 🚶‍♂️ステップ1 API管理画面で必要事項の登録

[デベロッパーポータル](https://developer.twitter.com/en/portal/dashboard)からOAuth2.0を有効にします。

有効化時、リダイレクトURLの登録が必要となります。後のステップを読んで決めてください。わからなければ `https://www.google.com` にしてもいいです。

有効化後に表示される `Client ID` と `Client Secret` をメモします。忘れても再発行できるので気楽に・・・

## 🚵 ステップ2 URL を作成する

アドレスは `https://twitter.com/i/oauth2/authorize` です。`?` 以降のクエリパラメータは次です。

|Key|Value|
| --- | --- |
|`response_type`|`code`でいいです。もっと知りたい方はOAuth2.0に詳しい人に聞いてください。|
|`client_id`|あなたの Client ID です。|
|`redirect_uri`|ユーザーが承認した後リダイレクトされるURIです。デベロッパーポータルで登録したものしか使えません。(後から追加もできます)|
|`scope`|ユーザーに承認してもらう権限です。[ここ](https://developer.twitter.com/en/docs/authentication/oauth-2-0/authorization-code#:~:text=Scopes%20allow%20you%20to%20set%20granular%20access%20for%20your%20App%20so%20that%20your%20App%20only%20has%20the%20permissions%20that%20it%20needs.%20To%20learn%20more%20about%20what%20scopes%20map%20to%20what%20endpoints,%20view%20our%20authentication%20mapping%20guide.)に一覧があるほか、ドキュメントの各API reference に必要なものが書いてあります。複数要求する場合はスペースで連結します。(最後にURLエンコードするのを忘れずに)|
|`state`|`state` でいいです。もっと知りた（略|
|`code_challenge`|最後のステップでアクセストークンを要求する際に必要になります。推奨される文字数はOAuth2.0に詳しい人に（略|
|`code_challenge_method`|`plain` でいいで（略|

完成したURLをアカウントの持ち主に開いてもらいます。

例(わかりやすいように改行しています)

https://twitter.com/i/oauth2/authorize
?response_type=code
&client_id=MY_CLIENT_ID
&redirect_uri=https%3A%2F%2Fwww.google.com
&scope=tweet.read+tweet.write+list.read+list.write+users.read+offline.access
&state=state
&code_challenge=SlVQjHz2
&code_challenge_method=plain

## 🚗 ステップ3 リダイレクトURLから`code`を取得する。

やることは一つです。リダイレクトURLのクエリパラメーターに `code=CODECODECODECODE` というものがあるので、これを取得してください。

方法によって様々なリダイレクトURIが考えられます。

- **自分でlocalhostのサーバーを立てて処理する**<br>この場合はリダイレクトuriは `http://localhost:8000` とかですかね。
- **URLスキームをレジストリで登録してプログラムで処理する**<br>この場合はリダイレクトuriは `my-cool-app://` などになりますね。
- **適当なサイトにリダイレクトしてURLを標準入力にコピペする**<br>一番簡単です。リダイレクト後のURLをユーザーにコピーさせて、プログラムに入力してもらいます。あとはプログラムで正規表現なりライブラリなりでクエリパラメータをパースしてcodeの値を手に入れます。

いずれの場合もURIの後ろにクエリ文字列 (`?state=state&code=CODECODE` のようなもの) を付与したリンクが開かれますので、`code` の値を取得してください。例えばURLに以下の正規表現でパターンマッチを取得することで、`code` の値を取得できます。

`code=([^?&]+)` (カッコの中が欲しいコードになります。)

## 🚅 ステップ4 `code` をつかってアクセストークンを得る。

APIエンドポイント `https://api.twitter.com/2/oauth2/token` に対して次のリクエストを `POST` します。

**headers**
|Key|Value|
|-|-|
|`Authorization`| `Basic` + (スペース) + base64Encode(Client ID + `:` + Client Secret)|
|`Content-Type`| `application/x-www-form-urlencoded` |

**Body** (content または Payload とも)

以下の `Key` と `value` のペアをURLエンコードした文字列

|Key|Value|
|-|-|
|`code`|取得したコード|
|`grant_type`|`authorization_code`|
|`redirect_uri`|ステップ2 で使用したリダイレクトURL|
|`code_verifier`|ステップ2 の `code_challenge` と全く同じもの|

注意事項として、リダイレクトされた時点から30秒以内にトークンを要求しないと `invalid_request` というメッセージとともに400番のレスポンスをされるということを書いておきます。

成功すると、json形式のレスポンスが帰ってきます。

```json
{
    "token_type": "bearer",
    "expires_in": 7200,
    "access_token": "ACCESS_TOKEN_ACCESS_TOKEN_ACCESS_TOKEN_ACCESS_TOKEN",
    "scope": "list.write list.read tweet.write users.read tweet.read"
}
```

無事にアクセストークンを入手することができました！このアクセストークンは2時間のみ有効です。

これを使って、APIのリクエストヘッダの `Authorization` に `Bearer ACCESS_TOKEN` と設定することで、ユーザーとしてツイートしたりDMしたりすることができます。各APIによって必要なscopeが違うので注意しましょう。

## 🚀 ステップ5 `refresh_token`で新しいアクセストークンを得る。

2時間じゃちょっと足りない！一度権限を許可したらずっとAPIを利用し続けたい！という人も居ると思います。

ステップ2でURLを作る際、`scopes` を指定しました。これに `offline.access` というスコープを追加すると、ステップ4のレスポンスjsonに `refresh_token` という値が追加されます。これを用いて新たに2時間有効な `access_token` を得ることができます。

ステップ4 とエンドポイント・ヘッダは同じですが、bodyが違います。

**Body** (content または Payload とも)

以下の `Key` と `value` のペアをURLエンコードした文字列

|Key|Value|
|-|-|
|`grant_type`|`refresh_token`|
|`refresh_token`|ステップ4で得た `refresh_token` |

これで、ステップ4と同じ形式のレスポンスを受け取ることができます。

# おわりに

以上になります。

1. URLを作成する。
1. ユーザーがログインして権限を承認する。
1. リダイレクトURLから code の値を取得する。
1. アクセストークンをリクエストする。
1. 必要に応じてアクセストークンをリフレッシュする。

これらのステップでアクセストークンを取得し、バンバン自動ツイートしていきましょう！

最初にも書きましたが筆者はOAuth初学者です。誤っている点があれば遠慮なく教えてください。

最後までお読み頂きありがとうございました。

# Pythonソース

自作のPython関数を雑に貼り付けます。大文字の変数は環境変数から読み込んだ定数です。

tweepy や oauth2lib などの認証をやってくれるライブラリは使いませんが、jsonパーサーやurl/base64のエンコード/デコードはライブラリにやってもらっています。

## 権限要求ページのURLを作る

```Python
def create_url():
    challenge = ''.join([random.choice('abcdefghijklmnopqrstuvwxyz') for _ in range(64)])
    with open('tmp/tw_challenge.txt', 'w') as f:
        f.write(challenge)  # 後で使うので保存
    scopes = ['tweet.read', 'tweet.write', 'users.read', 'offline.access']
    querys = {
        'response_type': 'code',
        'client_id': CLIENT_ID,
        'redirect_uri': REDIRECT_URI,
        'scope': ' '.join(scopes),
        'state': 'state',
        'code_challenge': challenge,
        'code_challenge_method': 'plain'
    }
    
    return 'https://twitter.com/i/oauth2/authorize' + '?' + parse.urlencode(querys)
```

## リダイレクトURLからcodeを取得する

```Python
def get_code_from_url(url):
    parsed_url = parse.urlparse(url)
    query = parsed_url.query
    query_dic = parse.parse_qs(query)
    return query_dic['code'][0]
```

## codeからアクセストークンをリクエストする(リフレッシュ兼用)

```Python
def get_tokens(code, refresh_token=None):
    auth = base64.b64encode(str(CLIENT_ID + CLIENT_SECRET).encode()).decode()
    headers = {
        'Authorization': auth,
        'Content-Type': 'application/x-www-form-urlencoded'
    }
    if not refresh_token:
        with open('tmp/tw_challenge.txt', 'r') as f:
            challenge = f.read().split()[0]  # code_challenge の読み込み
        data = {
            'code': code,
            'grant_type': 'authorization_code',
            'redirect_uri': REDIRECT_URI,
            'code_verifier': challenge
        }
    else:
        data = {
            'grant_type': 'refresh_token',
            'refresh_token': refresh_token
        }
    req = request.Request(
        'https://api.twitter.com/2/oauth2/token',
        headers=headers,
        data=parse.urlencode(data).encode()
    )
    with request.urlopen(req) as res:
        OA2token = json.loads(res.read())
    
    return OA2token
```