Bash とLinux にあるコマンドでOAuth 1.0 の認証の流れを理解する

# Bash とLinux にあるコマンドでOAuth 1.0 の認証の流れを理解する 
今となってはOAuth1.0 認証を実現したい時、ライブラリを使うことで、簡単に実装することができるようになりました。
ここでは、それらのライブラリを使わずに、OAuth1.0 の認証がどのようにして行われているかを理解するため、Twitter API を使って、その流れを見ていきたいと思います。
ということで、ここでは、なるべくシンプルにcurl コマンドを使ってTwitter にツイートする処理を参考にして、OAuth1.0 の認証処理の流れを見ていきたいと思います。。

注意点として、本記事はbash でTwitter クライアントを作成することではなく、処理を見ていくことが目的となります。
bash 等のコマンドラインからTwitter を利用したい場合は、twurl 等を使うようにしたほうが良いです。

- twitter/twurl OAuth-enabled curl for the Twitter API
    - https://github.com/twitter/twurl

# OAuth1.0 認証の流れ 
curl でTwitter へツイートする処理の概要としては、以下のようになります。

- 事前準備
    - コンシューマーキー、アクセストークンの取得

- リクエスト実行
    - nonce の作成
    - OAuth 署名方式の決定(HMAC-SHA1)
    - タイムスタンプの作成
    - OAuth バージョンの決定
    - OAuth 署名ベース文字列の作成
    - OAuth 署名鍵の作成
    - OAuth 署名の作成
    - ツイートパラメータ、OAuth パラメータを付与してcurl でリクエスト送信

端的に説明すると、OAuth1.0 の認証をパスするためには、リクエスのAuthorization ヘッダに各種OAuth パラメータを、カンマ区切りで付与することで可能となります。
しかし、このパラメータは、リクエストメソッドやURL、クエリストリングのパラメータや、リクエストボディ内のパラメータ等を使用して作成していく必要があります。
このAuthorization ヘッダの方法について、ひとつひとつ見ていきましょう。


```text:AuthorizationヘッダとOAuthパラメータ
Authorization: OAuth oauth_consumer_key="${oauth_consumer_key}",oauth_nonce="${oauth_nonce}",oauth_signature="${oauth_signature}",oauth_signature_method="HMAC-SHA1",oauth_timestamp="${oauth_timestamp}",oauth_token="${access_token}",oauth_version="1.0"
```


# curl でOAuth1.0 認証を実施して、ツイートする 
## コンシューマーキー、アクセストークンの取得 
事前に、Twitter Developer アカウントの登録とアプリケーションの登録を事前に済ませていることを前提とさせていただきます。
Twitter Developer アカウントの登録、アプリケーションの登録については、以下の記事が参考になります。

- Twitter API 登録 (アカウント申請方法) から承認されるまでの手順まとめ
    - https://qiita.com/kngsym2018/items/2524d21455aac111cdee

Developer アカウントを登録して、API key, API secret key, Access token, Access token secret を取得してください。

```bash
oauth_consumer_key='MyConsumerKeyyyyyyyyyyyyy'                                # API key
oauth_consumer_secret='MyConsumerSecrettttttttttttttttttttttttttttttttttt'    # API secret key
access_token='000000000000000000-MyAccessTokennnnnnnnnnnnnnnnnnn'             # Access token
access_token_secret='AccessTokenSecrettttttttttttttttttttttttttttt'           # Access token secret
```

OAuth コンシューマキーは、どのアプリケーションがリクエストをしているか、識別するために利用されます。

今回は基本的なbash とコマンドを使って、OAuth を通過し、TwitterJP からツイートを取得する例を参考に説明していきます。
この値(Developer console 上の名前ではAPI key)はDeveloper ポータルから取得することができます。

OAuth トークンの目的は、アカウントのアプリケーションの権限を可視化します(≒認可)。
これは、例えば閲覧のみを権限や、閲覧と書き込みの権限を持つ場合でトークンが変わってきます。
oauth_token はTwitter Developer ポータルの、ダッシュボードにて取得するもの(名前はAccess token)になります。

- Developer app dashboard
    - https://developer.twitter.com/en/apps

<table><tbody>
  <tr>
    <th> oauth_token</th>
    <td> 000000000000000000-MyAccessTokennnnnnnnnnnnnnnnnnn</td>
  </tr>
</tbody></table>
// Twitter Developer app ダッシュボードで`Access token` にあたるものになります

## URL 

```console
$ curl https://api.twitter.com/1.1/statuses/update.json
```


## Nonce(oauth_nonce) の作成 
リクエストの識別子となるNonce を作成します。Nonce の作成に厳密なルールはありませんが、乱数等をつかって、各リクエスト毎に一意となる値を指定するようにします。
これは、Twitter が同じリクエストが何度も送信されていないかを検出するために利用されます。
作成ルールは特に規定されていませんが、例えば32bit のランダムデータをbase64 エンコードして、記号類の文字を消去した値等が利用できます。


```console
$ oauth_nonce=$(head -c 32 < /dev/urandom | base64 | tr -d '+/=')
$ echo $oauth_nonce
x00fZ6JmXmXxFMjme6vfoBliWuMHQZmnjTbAW64iAg
```


これが、oauth_nonce となります。

<table><tbody>
  <tr>
    <th> oauth_nonce</th>
    <td> x00fZ6JmXmXxFMjme6vfoBliWuMHQZmnjTbAW64iAg</td>
  </tr>
</tbody></table>

## OAuth 署名方式(oauth_signature_method)の決定 
今回はTwitter のOAuth 1.0 を想定しているので、oauth_signature は一律`HMAC-SHA1`なります。

<table><tbody>
  <tr>
    <th> oauth_signature</th>
    <td> HMAC-SHA1</td>
  </tr>
</tbody></table>

## タイムスタンプ(oauth_timestamp) の作成 
この値は、いつリクエストが作成されたのかを識別するために用いられます。
形式はUnix エポック形式となっており、Twitter の場合、極端に昔に作成されたリクエストは拒否されるようになっています。
コマンドで作成する場合、date コマンドで以下のように作成できます。

```console
$ oauth_timestamp=$(date +%s)
$ echo $oauth_timestamp
1584272140
```

<table><tbody>
  <tr>
    <th> oauth_timestamp</th>
    <td> 1584272140</td>
  </tr>
</tbody></table>

## バージョン(oauth_version) 
OAuth のバージョンを指定します。Twitter のドキュメントにはOAuth 1.0a とありますが、ここでは一律1.0 を指定します。

<table><tbody>
  <tr>
    <th> oauth_version</th>
    <td> 1.0</td>
  </tr>
</tbody></table>

## OAuth 署名(oauth_signature) 
OAuth 署名の目的は、Twitter 側が、クライアントのリクエスト送信中に、パラメータ類の値が改ざんされていないかの検証に用いられます。
OAuth 署名は、oauth_signature 以外のパラメータ、HTTP リクエスト方式、URL 等の値から、署名アルゴリズムを通して作成されます。

OAuth 署名の作成は他の値と比較して、若干複雑なので、細かく分けて説明していきます。
作成の流れとしては

* HTTP リクエスト方式文字列の取得
* HTTP リクエストURL 文字列の取得
* HTTP リクエストパラメータの取得
* HTTP リクエストパラメータのソート・取得
* OAuth 署名作成のための鍵作成
* 署名作成

となります。

### HTTP リクエスト方式文字列の取得 
今回はHTTP のPOST メソッドでリクエストを飛ばします。署名を作成するための、HTTP リクエスト方式文字列は以下のとおりです。

<table><tbody>
  <tr>
    <th> HTTP リクエスト方式</th>
    <td> POST</td>
  </tr>
</tbody></table>

当然、GET メソッドの場合はGET となります。

### HTTP リクエスト文字列の作成 
まず、リクエスト先のURL を明確にします。今回のリクエスト先URL は以下のようになります。


```text
https://api.twitter.com/1.1/statuses/update.json
```


// GET メソッドの場合は、パラメータ(クエリストリング)を除去したURL になります

これをURL エンコードしたものがHTTP リクエストURL 文字列になります。

```console
$ url_string=$(echo -n "https://api.twitter.com/1.1/statuses/update.json" | curl -Gso /dev/null -w %{url_effective} --data-urlencode @- "" | cut -c 3-)
$ echo $url_string
https%3A%2F%2Fapi.twitter.com%2F1.1%2Fstatuses%2Fupdate.json
```

<table><tbody>
  <tr>
    <th> HTTP リクエストURL 文字列</th>
    <td> https%3A%2F%2Fapi.twitter.com%2F1.1%2Fstatuses%2Fupdate.json</td>
  </tr>
</tbody></table>

### パラメータ文字列の作成 
リクエストで飛ばすパラメータ文字列を作成します。
OAuth パラメータ(oauth_token は除く)と、POST するパラメータを洗い出します。


```text
status
oauth_consumer_key
oauth_token
oauth_nonce
oauth_timestamp             # 現在日付をepoch 形式で
oauth_signature_method      # "HMAC-SHA1" 固定
oauth_version               # "1.0" 固定
```

// 今回、OAuth 以外のパラメータがstatus しかありませんが、もちろん他のパラメータも一緒に送付する場合は、それも洗い出してください

これのパラメータを辞書順にソートします。
ソートした後、それぞれのパラメータを"&" で結合し、URL エンコードします。
パラメータ及び、パラメータの値がエンコード必要な形式(例えば日本語)の場合は、URL エンコード済みのものを指定するようにします。
例えば、スペースや特殊記号、日本語などは、URL エンコードしたものを指定するようにします。

[^1]

```console
$ read parameters_string < <(sort -t '=' -k1 << EOT \
        | sed -e  ':a' -e 'N' -e '$!ba' -e 's/\n/\&/g' | curl -Gso /dev/null -w %{url_effective} --data-urlencode @- "" | sed -E 's/..(.*).../\1/'
status=Test tweet from curl.
oauth_consumer_key=${oauth_consumer_key}
oauth_token=${access_token}
oauth_nonce=${oauth_nonce}
oauth_timestamp=${oauth_timestamp}
oauth_signature_method=HMAC-SHA1
oauth_version=1.0
EOT
)
```

上記の結果、作成されたパラメータ文字列は以下のようになります。


```console
$ echo $parameters_string
oauth_consumer_key%3Dz2xn65QI92jrxxCaP1YgWdF5e%26oauth_nonce%3Dx00fZ6JmXmXxFMjme6vfoBliWuMHQZmnjTbAW64iAg%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1584272140%26oauth_token%3D972390173848715264-8vX4rlYj8ZmclRAmGjop1lsYKildbvh%26oauth_version%3D1.0%26status%3DThis%20is%20a%20tweet%20from%20pure%20curl.
```


補足として、もし重複するパラメータがあった場合、OAuth の規定では、それらについては値でソートするようになっています。
しかし、Twitter の場合は、重複するパラメータキーがあった場合は、許可しないようになっているようです。

### 署名ベース(signature base)文字列の作成 
署名を行うベース文字列は、「HTTP リクエスト方式」、「HTTP リクエストURL」、「パラメータ文字列」を"&" で連結して作成します。

```console
$ signature_base_string="POST&${url_string}&${parameters_string}"
$ echo $signature_base_string
POST&https%3A%2F%2Fapi.twitter.com%2F1.1%2Fstatuses%2Fupdate.json&oauth_consumer_key%3Dz2xn65QI92jrxxCaP1YgWdF5e%26oauth_nonce%3Dx00fZ6JmXmXxFMjme6vfoBliWuMHQZmnjTbAW64iAg%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1584272140%26oauth_token%3D972390173848715264-8vX4rlYj8ZmclRAmGjop1lsYKildbvh%26oauth_version%3D1.0%26status%3DThis%20is%20a%20tweet%20from%20pure%20curl.
```


### 署名鍵の作成 
署名を行うための鍵を作成します。鍵はoauth_consumer_secret(API secret key)と、access_token_secret(Access token Secret)を"&"で結合した文字列になります。


```console
$ signing_key="${oauth_consumer_secret}&${access_token_secret}"
$ echo $signing_key
MyConsumerSecrettttttttttttttttttttttttttttttttttt&AccessTokenSecrettttttttttttttttttttttttttttt
```

<table><tbody>
  <tr>
    <th> 署名鍵</th>
    <td> MyConsumerSecrettttttttttttttttttttttttttttttttttt&AccessTokenSecrettttttttttttttttttttttttttttt</td>
  </tr>
</tbody></table>

### 署名の作成 
署名ベース文字列と署名鍵を使って、HMAC-SHA1 で署名を作成します。そして、それをURL エンコードします。これがoauth_signature に設定される値となります。


```console
$ oauth_signature="$(echo -n "${signature_base_string}" | openssl dgst -sha1 -binary -hmac "${signing_key}" | base64 | curl -Gso /dev/null -w %{url_effective} --data-urlencode @- "" | sed -E 's/..(.*).../\1/')"
$ echo $oauth_signature
qjPJJq%2Fhihh0Sz3x0BN591deW%2BM%3D
```

<table><tbody>
  <tr>
    <th> oauth_token</th>
    <td> qjPJJq%2Fhihh0Sz3x0BN591deW%2BM%3D</td>
  </tr>
</tbody></table>

## curl コマンドでツイートする 
これで、OAuth1.0 認証を突破して、ツイートをする準備ができました。
実際にcurl でリクエストするときは、作成してきたパラメータを、Authorization ヘッダにカンマ区切りで設定してリクエストを送信します。
具体的にはいかのようになります。


```console
$ curl -X POST --url 'https://api.twitter.com/1.1/statuses/update.json' \
    -d "status=Test tweet from curl." \
    --header "Authorization: OAuth oauth_consumer_key=\"${oauth_consumer_key}\", oauth_nonce=\"${oauth_nonce}\", oauth_signature=\"${oauth_signature}\", oauth_signature_method=\"HMAC-SHA1\", oauth_timestamp=\"${oauth_timestamp}\", oauth_token=\"${access_token}\", oauth_version=\"1.0\""
```


これで、Twitter 上にツイートが表示されるはずです。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">curl コマンドからtweet テスト</p>&mdash; tsutomu (@tsuna0x00) <a href="https://twitter.com/tsuna0x00/status/1242802202403459073?ref_src=twsrc%5Etfw">March 25, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

もし認証が通らない場合、署名作成に失敗していることが多いかと思います。
正しい署名が作成されているかどうか確認したい場合は、デバッグのため、oauth_nonce、oauth_timestamp に固定の値を使うようにします。
そのうえで、Postman を使ってリクエストをして、その時のoauth_signature と自分が作成したoauth_signature が一致するかを確認することをおすすめします。

## おまけ: ツイートbash 関数を作成する 
これらの処理を関数としてまとめてみました。


```bash
url_encode() {
    curl -Gso /dev/null -w %{url_effective} --data-urlencode @- "" | sed -E 's/..(.*).../\1/'
}

function join_by {
    local d=$1
    shift
    echo -n "$1"
    shift
    printf "%s" "${@/#/$d}"
}

tweet() {
    local url="https://api.twitter.com/1.1/statuses/update.json"
    local oauth_parameters=(
        "oauth_consumer_key=$1"
        "oauth_token=$3"
        "oauth_timestamp=$(date +%s)"
        "oauth_nonce=$(head -c 32 < /dev/urandom | base64 | tr -d '+/=')"
        "oauth_signature_method=HMAC-SHA1"
        "oauth_version=1.0"
    )
    local parameters=("status=$(echo "$5" | url_encode)")
    local oauth_consumer_secret="$2"
    local access_token_secret="$4"

    local oauth_signature=$(sort -t '=' -k1 << END \
                              | sed -e  ':a' -e 'N' -e '$!ba' -e 's/\n/\&/g' | url_encode \
                              | echo -n "POST&$(echo "$url" | url_encode)&$(</dev/stdin)" \
                              | openssl dgst -sha1 -binary -hmac "${oauth_consumer_secret}&${access_token_secret}" \
                              | base64 | url_encode
$(join_by $'\n' "${oauth_parameters[@]}")
$(join_by $'\n' "${parameters[@]}")
END
)

    curl -X POST --url "$url" \
        -d "$(join_by '&' ${parameters[@]})" \
        --header "Authorization: OAuth $(join_by ',' ${oauth_parameters[@]}),oauth_signature=${oauth_signature}"
}
```


このようにbash で関数を定義した後、以下のように関数を実行すれば、ツイートできるようになります。


```console
$ tweet "MyConsumerKeyyyyyyyyyyyyy" "MyConsumerSecrettttttttttttttttttttttttttttttttttt" "000000000000000000-MyAccessTokennnnnnnnnnnnnnnnnnn" "AccessTokenSecrettttttttttttttttttttttttttttt"
```

# 参考 
- Authentication - OAuth 1.0a - Twitter Developers
    - https://developer.twitter.com/en/docs/basics/authentication/oauth-1-0a

- Creating a signature - OAuth 1.0a - Twitter Developers
    - https://developer.twitter.com/en/docs/basics/authentication/oauth-1-0a/creating-a-signature

- Get Tweet timelines
    - https://developer.twitter.com/en/docs/tweets/timelines/api-reference/get-statuses-user_timeline

- シェルスクリプトでシンプルにurlエンコードする話
    - https://qiita.com/ik-fib/items/cc983ca34600c2d633d5

- シェルスクリプト（Bash）で作るTwitterクライアント
    - https://qiita.com/piroor/items/a999eeef9a67283b1e56

- How to urlencode data for curl command?
    - https://stackoverflow.com/questions/296536/how-to-urlencode-data-for-curl-command

- OAuthについて１から勉強した
    - https://qiita.com/takecho123/items/42cdf39a3aff2a441374

- How can I join elements of an array in Bash?
    - https://stackoverflow.com/questions/1527049/how-can-i-join-elements-of-an-array-in-bash

[^1]: 色々試してみたところ、半角スペースについてはURL エンコードしなくても通ることが確認できました。ただ、今回は手順を統一化するために、URL エンコードして実施することにします。