TwitterAPIをOAuth2.0 user contextで叩く

# はじめに
TwitterAPIのOAuth2.0 user contextはTwitterAPIv2.0しか使えないデメリットがありますが、Essentialレベルでも使えるのは大きなメリットです。
client_idやclient_secretの取得は既に出来ている前提です。

- [RFC7636](https://datatracker.ietf.org/doc/html/rfc7636)
- [Twitter developer platform](https://developer.twitter.com/en/docs/authentication/oauth-2-0)
- [OAuth 2.0 認可コードフロー+PKCE をシーケンス図で理解する](https://zenn.dev/zaki_yama/articles/oauth2-authorization-code-grant-and-pkce)

を参考にしました。
サンプルコードはGASです。フルスクラッチです。
# アクセストークンの取得手順
1.ユーザーに踏ませる認可URLを作成する
2.リダイレクトのパラメータを用いてアクセストークンを取得する

以上です。

# ユーザーに踏ませる認可URLを作成する
TwitterのOAuth2の認可URLは`https://twitter.com/i/oauth2/authorize` です。これにクエリーパラメータを付け足していきます。
パラメータは以下の7つです。
- `response_type`: 必ず`code`にしなければなりません
- `client_id`
- `redirect_uri`: 事前に設定したものと同じ出なければなりません
- `scope`:後述
- `state`:後述
- `code_chellange`:後述
- `code_chellange_method`:`plain`か`S256`です。セキュリティ上`S256`一択です。

#### scope
[一覧表](https://developer.twitter.com/en/docs/authentication/oauth-2-0/authorization-code#:~:text=a%20public%20client.-,Scopes,-Scopes%20allow%20you)から選んで「+」で連結します。
```
scope=tweet.write+tweet.read+users.read+offline.access
```
#### state
リクエストとリダイレクトの間で状態を維持するために使用するランダムな値です。リダイレクト時にそのまま帰ってきます。これを用いてどのユーザーのリダイレクトなのか判別します。

GASにはリダイレクト用の便利な関数があるのでそれを使用します。
リダイレクトを受け取る関数やリダイレクト事に追加されるパラメータを紐づけた`state`を作成してくれます。
リダイレクトURLは`https://script.google.com/macros/d/{スクリプトID}/usercallback`にする必要があります。
```js:GAS
const state = ScriptApp.newStateToken()
  //リダイレクトを受け取る関数名
  .withMethod("authCallBack")
  .withTimeout(3600)
  //リダイレクト時のパラメータの追加
  .withArgument("key", value)
  .createToken();
```
#### code_challenge
`code_challenge`を作成するにはまず`code_verifier`を作成します。特に`code_verifier`に作成方法の指定はありません。手順2で`code_veirifer`を再度使用するので`state`と紐づけて保存しておく必要があります。

その`code_verifier`を用いて`code_challenge`を算出します。
`code_challenge_method`が`plain`の時は、`code_challenge`は`code_verifier`そのものです。
`code_challenge_method`が`S256`の時は、`code_verifer`を`SHA-256`でハッシュ化してそれをbase64URLエンコードしたものが`code_challenge`になります。

サンプルコードの`code_challenge_method`は`SHA256`です
```js:GAS
//a-zまでのランダム*32文字
const code_verifier=Array(32).fill(0).map(()=>String.fromCharCode(Math.random()*26+97)).join("")
const hashed=
Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256,code_verifier,Utilities.Charset.US_ASCII)
const challenge=Utilities.base64EncodeWebSafe(hashed).replace(/=/g,"")
//パディングの「=」が消えないので、自分で消す
//stateとcode_verifierを紐づけて保存する。state作成時のwithArgumentを使用するのもアリです。
ScriptProperties.setProperty(state,code_verifier)
```

これでパーツは全部揃ったのでURLエンコードして組み立てます。視認性向上のため改行が入っています。
```
https://twitter.com/i/oauth2/authorize?
response_type=code&
client_id=client_id&
redirect_uri=https%3A%2F%2F...&
scope=tweet.write+tweet.read+users.read+offline.access&
state=abcdefg&
code_challenge=aiueo&
code_challenge_method=S256
```

# リダイレクトのパラメータを用いてアクセストークンを取得する
リダイレクト時に付いてくるパラメータは以下の2つです。
- `code`
- `state`:手順1で作成したものが帰ってきます。

`state`を用いてどのユーザーのリダイレクなのかを判別して、そのユーザー用に発行した`code_verifer`と、パラメータの`code`を用いてアクセストークンを取得します。
トークン取得エンドポイントは`https://api.twitter.com/2/oauth2/token`です。
必要パラメータは以下の5つです。
- `grant_type`:必ず`authorization_code`にしなければならない
- `code`:受け取った物をそのまま返します。
- `redirect_uri`:事前に設定したリダイレクトURLと同じでなければなりません
- `client_id`
- `code_verifier`:手順1で作成したものです。

```js:GAS
function authCallBack(e){
  const {code,state}=e.parameter
  const options={
    method:"post",
    contentType:"application/x-www-form-urlencoded",
    //confidential clientの場合必須。client_id、client_secretはurlエンコード推奨です。長いので省略しました
    headers:{Authorization: "Basic "+Utilities.base64Encode(client_id+":"+client_secret)},
    payload:{
      grant_type:"authorization_code",
      code:code,
      redirect_uri:redirect_uri,
      client_id:client_id,
      code_verifier:ScriptProperties.getProperty(state)
    }
  }
  const response=UrlFetchApp.fetch("https://api.twitter.com/2/oauth2/token",options)
}
```

レスポンスはJSONで帰ってきます。
```json
{
  "token_type": "bearer",
  "access_token": "MEf...",
  "refresh_token": "bWZ...",
  "expires_in": 7200,
  "scope": "tweet.write tweet.read users.read offline.access"
}
```
- `token_type`:基本常に`bearer`です。`bearer`であることを確認してください。
- `access_token`:アクセストークンです。使用方法は後述します。
- `refresh_token`:`scope`に`offline.access`があれば帰ってきます。有効期限を更新出来ます。使用方法は後述します。
- `expires_in`:有効期限(秒)です。
- `scope`:手順2で作成した`scope`が帰ってきます。

アクセストークンとリフレッシュトークンは環境変数に保存しておきましょう。

# TwitterAPIを叩く
OAuth1と違い結構簡単です。ヘッダーにBearer認証を追加するだけです。
```js
const options={
  method:"post",
  headers:{Authorization:"Bearer "+access_token},
  contentType:"application/json",
  payload:JSON.stringify({
    text:"あ"
  })
}
const response=UrlFetchApp.fetch("https://api.twitter.com/2/tweets",options)
```
```json
{"id":1515156578613317635, "text":"あ"}
```

# トークンをリフレッシュする
このままだと2時間でトークンが失効するので定期的にリフレッシュする必要があります。スコープに`offline.access`を入れるのを忘れないで下さい。

```js
const options={
  method:"post",
  headers:{
    //作成方法はトークン取得時のと同じなので割愛
    Authorization:"Basic "+basic
  },
  contentType:"application/x-www-form-urlencoded",
  payload:{
    grant_type:"refresh_token",
    refresh_token:refresh_token
   }
}
const response=UrlFetchApp.fetch(`https://api.twitter.com/2/oauth2/token`,options)
```
レスポンスのJSON構造はトークン取得時のと全く同じです。

# 終わりに
途中でエラーが出たらUrlFetchApp.getRequestでリクエスト内容を見ることが出来るので確認してみてください。大体2回URLエンコードしてるとかです(2敗)