Help us understand the problem. What is going on with this article?

【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モジュール のtwitterがあります
こちらのモジュールはすでに認可済みのOAuthトークンを利用し,
OAuthの認証フローに基づいたツイートをPOSTするリクエスト送信したり,
タイムラインをGETするリクエストを送信したりするためのモジュールになります

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

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 の認証フローについてですが,
以下で説明がされているので引用します
Yahoo!デベロッパーネットワーク: OAuthの全体フロー
https://developer.yahoo.co.jp/other/oauth/flow.html

中間サーバの処理フロー

前提知識として各種トークンのざっくりとした説明を以下の表に示します

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

request_tokenを取得する処理を行うためのリクエストを中間サーバに向けて発行します
:リクエストを受けて中間サーバはTwitterのAPIを利用し, request_tokenを取得する処理を行います
request_tokenを取得後, request_tokenを利用して、クライアントへ認証するためのURIをレスポンスします
:ユーザが認証を終えた後, コールバックURLとしてサーバのURLを設定し, URIに設定されている認可前のaccess_tokenoauth_verifierをサーバに保持します
:保持したrequest_tokenoauth_verifierを利用し, 認可済みのacceess_tokenを取得します
:最後にクライアントの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_tokenaccees_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を受け取り保持します

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

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_tokenoauth_verifierを利用し,
中間サーバTwitterAPI経由で, access_tokenの取得を試みます

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

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

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 URLTwitterAPI用の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();
    }
}

クリエイティブ・コモンズ・ライセンス
この 作品 は クリエイティブ・コモンズ 表示 - 非営利 4.0 国際 ライセンスの下に提供されています。

おわりに

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

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした