【Node.js】TwitterAPI OAuth1.0 アクセストークン取得用の中間サーバを構築しよう

# 自己紹介
都内で "新卒1年目" のフロントエンドエンジニア🐈（エンジニアは猫なので）やってるもので,
JavaScriptでの開発歴でいうとちょうど2年くらいです
部署に配属してから "ようやく" 4ヶ月過ぎたくらいですかね？
レガシー案件もそこそこ多いので適当にやり過ごしてます（保守も🐈の仕事なのです）
にゃーん🐈と鳴いているので新卒だけどお声がけあればいいなあとか, 
ゲスい考えしてこの記事も書いてたりします（おい）
大学の時もいろいろやっていましたが本記事では身バレしそうなので秘密です

# OAuth1.0 認可済みのトークン取得用の中間サーバの構築
API といえばよくあげられるのが TwitterAPI ですよね？（勝手な思い込み）
そういうわけで今回は,
Node.js で TwitterAPI 用の OAuth1.0の認証フローに沿って, 
認可済みのトークンを取得する中間サーバを構築していきます
なんで Node.js なの？というと
単純に TwitterAPI 用のサーバを説明している記事の中に 
Node.js でサーバを構築している記事が少ないからです
以前, 似ている記事を書いたのですが, かなり説明を端折った箇所があるのと,
ソフトウェア構成がおざなりになっていたため構成など改めてきちんと書いていきます

それとこれは個人的にですがフロントエンドエンジニアなのに
マークアップ案件多すぎて鬱憤晴らしに書いてるフシもあります

## 前提知識など
1. npm や Node.js の環境構築が可能
2. Node.js の APIサーバ を構築可能
3. Node.js における HTTPリクエスト処理 について理解がある
4. TwitterDev に登録し開発者用のコンシューマキーなどを取得済み

## はじめに
OAuth1.0の認証フローに沿った中間サーバと言いますが,
OAuthの認証によく使われる npmモジュール の<a href="https://www.npmjs.com/package/twitter">twitter</a>があります
こちらのモジュールはすでに認可済みのOAuthトークンを利用し,
OAuthの認証フローに基づいたツイートをPOSTするリクエスト送信したり, 
タイムラインをGETするリクエストを送信したりするためのモジュールになります

ちなみに今回したいのは, 
<b>OAuth認証フローを利用した 認可済みのOAuthトークン（アクセストークン）の取得</b>
です

## TwitterAPI について
Twitterが提供しているTwitter上に表示されているタイムラインを取得できたり,
Twitterに向けてツイートを投稿したりできる, インターフェースです
TwitterAPI の OAuthの認証フロー は, 一部分のみ OAuth2.0 が採用されていて,
全てのAPIの機能を利用するには未だ OAuth1.0 を利用する必要があります
そのため今回は OAuth1.0 の認証フロー用の中間サーバを構築します
それと, OAuth2.0 については他の方が書くみたいなのでそちらへ
https://qiita.com/advent-calendar/2019/identity

APIの仕様については, こちらを参照してください
https://developer.twitter.com/en/docs/basics/authentication/api-reference

処理フローについては実装の章でソースコードに沿って説明します

## OAuth1.0 認証フロー
OAuth1.0 の認証フローについてですが,
以下で説明がされているので引用します
<b>Yahoo!デベロッパーネットワーク: OAuthの全体フロー</b>
https://developer.yahoo.co.jp/other/oauth/flow.html
<img style="width: 556;"src="https://s.yimg.jp/images/login/docs/ydn/oauth/oauth1a_session_flow.png">

## 中間サーバの処理フロー
前提知識として各種トークンのざっくりとした説明を以下の表に示します

| KeyToken | 役割 |
|:-----------|:------------|
| consumer_key | 各アプリごとにTwitterから発行されるアプリの識別キー |
| consumer_secret | 各アプリごとにTwitterから発行されるアプリの識別キー |
| oauth_token | 認証用のトークン| 
| oauth_token_secret | 認証用のトークン |
| oauth_verifier | 認証が完了しているか確認するためのトークン | 
| request_token | 認可される前のoauth_token |
| accsess_token | 認可済みのoauth_token |

<b>①</b>：`request_token`を取得する処理を行うためのリクエストを中間サーバに向けて発行します
<b>②</b>：リクエストを受けて中間サーバはTwitterのAPIを利用し, `request_token`を取得する処理を行います
<b>③</b>：`request_token`を取得後, `request_token`を利用して、クライアントへ認証するためのURIをレスポンスします
<b>④</b>：ユーザが認証を終えた後, コールバックURLとしてサーバのURLを設定し, URIに設定されている認可前の`access_token`と `oauth_verifier`をサーバに保持します 
<b>⑤</b>：保持した`request_token`と`oauth_verifier`を利用し, 認可済みの`acceess_token`を取得します
<b>⑥</b> ：最後にクライアントのURLへユーザをリダイレクトさせます

※ 注記
⑤の処理とその後のトークン管理については今回は言及しません
データベースにユーザ名と`oauth_verifier`を一緒にハッシュ化してキーにするとかですかね？

気になる方はこちらの記事を読んでみるといいかも？
https://qiita.com/TakahikoKawasaki/items/00f333c72ed96c4da659
私も気になって調べたらよさげな記事があったので（別言語なのと良し悪しに責任はもちませんが...）

## 実装

まず, 必要なnpmモジュールをインストールし, 

```
npm i http crypto querystring axios
```
その後, これらのモジュールをインポートします

```index.js
const http = require('http')
      crypto = require('crypto')
      qs = require('querystring')
      axios = require('axios');
```

また, 必要な定数をグローバル変数として, 定義しておきます
※ クラスや別ファイルに定義してカプセル化などした方がいいと思いますが, ここでは簡易に

```index.js
const getRequestTokenUrl = 'https://api.twitter.com/oauth/request_token';
const getAccessTokenUrl = 'https://api.twitter.com/oauth/access_token';
const callbackUrl = '<Sever URI>';
const consumerKey = 'xxxxx';
const consumerSecret = 'xxxxx';
const keyOfSign = encodeURIComponent(consumerSecret) + '&';
```

次に認証認可に必要なメソッドとプロパティを定義するクラスを用意します。
順に追って説明していきます。

まず, 取得した`request_token`や`accees_token`などを格納するためのプロパティを,
コンストラクタで定義し, プロパティを扱うための`setter`, `getter`を定義します

```RequestTokenMethods.js
class RequestTokenMethods {
    constructor() {    
        this.dataOAuth = {
            oauthToken: '',
            oauthTokenSecret: '',
            oauthVerifier: '',
            // oauthVerifierとユーザ名のハッシュ化した値の保持などに利用
            oauthHashKey: '',
        };
    }
    getOAuthData() {
        return this.dataOAuth;
    }
    setOAuthData(props, reqProps = 'oauthToken') {
        switch (reqProps) {
            case 'oauthToken': this.dataOAuth.oauthToken = props;
            case 'oauthTokenSecret': this.dataOAuth.oauthTokenSecret = props;
            case 'oauthVerifier': this.dataOAuth.oauthVerifier = props;
            case 'oauthHashKey': this.dataOAuth.oauthHashKey = props;
            default: return;
        }
    }
    ...
}
```
続いて`request_token`をTwitterAPI経由で取得するためのメソッドを定義していきます

まず,`getRequestToken()`について説明します
`getRequestToken()`は引数に設定されたクエリパラメータを利用して生成したURLを
リクエストヘッダに紐づけ, Twitterに向けて`POSTリクエスト`を送信します
その後, レスポンスとして設定されている`request_token`を受け取り保持します

`引数に設定するクエリパラメータ`を以下に示します

```js
const paramsRequestToken = {
    oauth_callback: callbackUrl,
    oauth_consumer_key: consumerKey,
    oauth_signature_method: 'HMAC-SHA1',
    oauth_timestamp: (() => {
        const date = new Date();
        return Math.floor(date.getTime() / 1000);
    })(),
    oauth_nonce: (() => {
        const date = new Date();
        return date.getTime();
    })(),
    oauth_version: '1.0',
};
```

以上のクエリパラメータをソートし, ソートしたクエリパラメータから認証用のURLを作成します。
その次に, 認証に必要なリクエストヘッダを作成し, 
`POSTリクエスト`を送信した後にレスポンスデータに含まれる,
`request_token`を保持するメソッドを定義します

```RequestTokenMethods.js
async getRequestToken(params){
    Object.keys(params).forEach(item => {
        params[item] = encodeURIComponent(params[item]);
    });

    let requestParams = Object.keys(params).map(item => {
        return item + '=' + params[item];
    });
    requestParams.sort((a, b) => {
        if (a < b) return -1;
        if (a > b) return 1;
        return 0;
    });
    requestParams = encodeURIComponent(requestParams.join('&'));

    const dataOfSign = (() => {
       return encodeURIComponent('POST') + '&' + encodeURIComponent(getRequestTokenUrl) + '&' + requestParams;
    })();

    const signature = (() => {
        return crypto.createHmac('sha1', keyOfSign).update(dataOfSign).digest('base64');
    })();

    params['oauth_signature'] = encodeURIComponent(signature);

    let headerParams = Object.keys(params).map(item => {
        return item + '=' + params[item];
    });

    headerParams = headerParams.join(',');

    const header = {
        'Authorization': 'OAuth ' + headerParams
    };

    //オプションを定義
    const options = {
        url: getRequestTokenUrl,
        headers: header,
    };
    //リクエスト送信
    return await this.getTokenSync(options)
}
```

続いて`access_token`をTwitterAPI経由で取得するためのメソッドを定義していきます
まず, `getAccessToken()`について説明します
クライアント側でユーザが認証を終えて, Twitterの認証画面から, 
サーバのクエリパラメータを含んだURIに遷移します
アクセスされた際は,`GETリクエスト`がサーバに対してリクエストされています
そのリクエストを検知した後, クエリパラメータを取得し, 保持します

その後, `getAccessToken()`は, 
取得したクエリパラメータの`request_token`と`oauth_verifier`を利用し,
中間サーバTwitterAPI経由で, `access_token`の取得を試みます

一連の処理が終わると, ユーザをリダイレクトで元のクライアントのURLへ遷移させます

`access_token` を取得するために利用する,
引数に設定するクエリパラメータを以下に示します

```js
const requestMethod = new RequestTokenMethods();
const paramsAccessToken = {
    consumer_key: consumerKey,
    oauth_token: requestMethod.oauth_token,
    oauth_signature_method: 'HMAC-SHA1',
    oauth_timestamp: (() => {
        const date = new Date();
        return Math.floor(date.getTime() / 1000);
    })(),
    oauth_verifier: requestMethod.oauth_verifier,
    oauth_nonce: (() => {
        const date = new Date();
        return date.getTime();
    })(),
    oauth_version: '1.0',
};
```

`getAccessToken()`のメソッドは以下の通りです
`getRequestToken()`との差異は, リクエストURLが違うくらいなので,
見やすくすることを除けばリクエスト名でURLをハンドリングするほうがいいかもしれません...

```RequestTokenMethods.js
 async getAccessToken(params){
    Object.keys(params).forEach(item => {
        params[item] = encodeURIComponent(params[item]);
    });

    let requestParams = Object.keys(params).map(item => {
        return item + '=' + params[item];
    });

    requestParams.sort((a, b) => {
        if (a < b) return -1;
        if (a > b) return 1;
        return 0;
    });
    requestParams = encodeURIComponent(requestParams.join('&'));
    const dataOfSign = (() => {
        return encodeURIComponent('POST') + '&' + encodeURIComponent(getAccessTokenUrl) + '&' + requestParams;
    })();

    const signature = (() => {
        return crypto.createHmac('sha1', keyOfSign).update(dataOfSign).digest('base64');
    })();

    params['oauth_signature'] = encodeURIComponent(signature);

    let headerParams = Object.keys(params).map(item => {
        return item + '=' + params[item];
    });

    headerParams = headerParams.join(',');

    const header = {
        'Authorization': 'OAuth ' + headerParams
    };

    //オプションを定義
    const options = {
        url: getAccessTokenUrl,
        headers: header,
    };
    //リクエスト送信
    return this.getTokenSync(options)
}
```

`getTokenSync()`は, `getRequestToken()`と`getAccessToken()`内で使用され, 
 Twitter API に向けて`axios`を利用し, 同期的にAPI通信を行うためのメソッドです

```RequestTokenMethods.js
    async getTokenSync(options){
        return axios.post(options.url, options.headers)
        .then(res => {
            const tmpData = {
                oauth_token: qs.parse(res.data).oauth_token,
                oauth_token_secret: qs.parse(res.data).oauth_token_secret,
            }
            return tmpData;
        })
        .catch(err => {
            throw err
        })
    }
}
```

ようやくサーバ本体のスクリプトですが...
まず, レスポンスヘッダを設定します
レスポンスヘッダの設定の際に`Access-Control-Allow-Origin`の指定に, 
`Client URL`と`TwitterAPI用のURL`を追加します
その他はの設定は, 自由にカスタマイズしてやってください

```index.js
const httpServer = new http.createServer((req, res) => {
    res.setHeader('Access-Control-Allow-Origin', 'https://api.twitter.com/*');
    res.setHeader('Access-Control-Allow-Origin', '<Client URL>');
    res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
    ...
}).listen(process.env.PORT === true ? process.env.PORT : 8080);;
```

次にサーバスクリプトのリクエスト処理についてです
流れは単純で`POSTリクエスト`と`GETリクエスト`を判別し,
それぞれに, `getRequestToken()`と`getAccessToken()`の説明で書いた処理を行っていきます
※ 一応, 追記ですがセキュリティ関連の処理をほぼつけていないので, ホワイトリストやIP制御など,
必要な処理は各々でカスタムしてみてください

```index.js
if (req.method === 'POST') {
    req.on('data', (data) => {
        const resData = data + '';
        const paramsRequestToken = {
            oauth_callback: callbackUrl,
            oauth_consumer_key: consumerKey,
            oauth_signature_method: 'HMAC-SHA1',
            oauth_timestamp: (() => {
                const date = new Date();
                return Math.floor(date.getTime() / 1000);
            })(),
            oauth_nonce: (() => {
                const date = new Date();
                return date.getTime();
            })(),
            oauth_version: '1.0'
        };
        const requestMethod = new RequestTokenMethods();
        if (resData === 'request_token' && requestMethod.getOAuthData().oauthVerifier !== '') {
            requestMethod.getRequestToken(paramsRequestToken)
            .then((tokenOAuth) =>{
                const oauthUri = 'https://api.twitter.com/oauth/authorize?oauth_token=' + encodeURIComponent(tokenOAuth.oauth_token),

                req.on('end', () => {
                    res.writeHead(200, {
                        'Content-Type': 'application/json'
                    });
                    res.write(oauthUri);
                    res.end();
                });
            }).catch(err => {
                console.log(err);
                res.writeHead(408, {
                  'Content-Type': 'application/json'
                });
                res.write('error!');
                res.end();
            });
        }
    )}
}　else if (req.method === 'GET') {
    if ((window.location.search + '').match(/oauth_verifier/)) {
        const getQueryVariable = (variable) => {
            const reqURI = req.protocol + '://' + req.headers.host + req.url;
            const query = reqURI.substring(1);
            const varbs = query.split('&');
            varbs.forEach(varb, () => {
                const pair = varb.split('=');
                if (pair[0] === variable) {
                    return pair[1];
                }
            })
        }
        const requestMethod = new RequestTokenMethods();
        const dataOAuthToken = requestMethod.getOAuthData();
        requestMethod.setOAuthData(getQueryVariable('oauth_verifier'), 'oauthVerifier');
        requestMethod.setOAuthData(getQueryVariable('oauth_token'), 'oauthToken');

        const paramsAccessToken = {
            consumer_key: consumerKey,
            oauth_token: requestMethod.oauthToken,
            oauth_signature_method: 'HMAC-SHA1',
            oauth_timestamp: (() => {
                const date = new Date();
                return Math.floor(date.getTime() / 1000);
            })(),
            oauth_verifier: requestMethod.oauthVerifier,
            oauth_nonce: (() => {
                const date = new Date();
                return date.getTime();
            })(),
            oauth_version: '1.0',
        };

        requestMethod.getAccessToken(paramsAccessToken)
            .then((tokenOAuth) => {
            requestMethod.setOAuthData(tokenOAuth.oauthToken, 'oauth_token');
            requestMethod.setOAuthData(tokenOAuth.oauthTokenSecret, 'oauth_token_secret');

            const keyOfToken = crypto.createHmac('sha256', dataOAuthToken.oauthVerifier + token.oauthToken);
            requestMethod.setOAuthData(keyOfToken.update().digest('base64'), 'oauthHashKey');

        res.writeHead(302, {
            'Location': '<Client URL>' + encodeURIComponent('?id=' + dataOAuthToken.oauthHashKey),
        });
        res.write('redirect!');
        res.end();

    } else {
        res.writeHead(408, {
            'Content-Type': 'application/json'
        });
        res.write('error!');
        res.end();
    }
}
```
<a rel="license" href="http://creativecommons.org/licenses/by-nc/4.0/"><img alt="クリエイティブ・コモンズ・ライセンス" style="border-width:0" src="https://i.creativecommons.org/l/by-nc/4.0/88x31.png" /></a><br />この 作品 は <a rel="license" href="http://creativecommons.org/licenses/by-nc/4.0/">クリエイティブ・コモンズ 表示 - 非営利 4.0 国際 ライセンス</a>の下に提供されています。

##おわりに
アドベントカレンダーなどに自分の記事を書くのは初めてでしたので,
拙い点などあるかもしれませんが, マサカリなどありましたらお手柔らかにお願いしますね

※ 時間がなかったためGETリクエストの辺りは空デバッグ
※ もしミスなどありましたらよろしくお願いします