自己紹介
都内で "新卒1年目" のフロントエンドエンジニア🐈(エンジニアは猫なので)やってるもので,
JavaScriptでの開発歴でいうとちょうど2年くらいです
部署に配属してから "ようやく" 4ヶ月過ぎたくらいですかね?
レガシー案件もそこそこ多いので適当にやり過ごしてます(保守も🐈の仕事なのです)
にゃーん🐈と鳴いているので新卒だけどお声がけあればいいなあとか,
ゲスい考えしてこの記事も書いてたりします(おい)
大学の時もいろいろやっていましたが本記事では身バレしそうなので秘密です
OAuth1.0 認可済みのトークン取得用の中間サーバの構築
API といえばよくあげられるのが TwitterAPI ですよね?(勝手な思い込み)
そういうわけで今回は,
Node.js で TwitterAPI 用の OAuth1.0の認証フローに沿って,
認可済みのトークンを取得する中間サーバを構築していきます
なんで Node.js なの?というと
単純に TwitterAPI 用のサーバを説明している記事の中に
Node.js でサーバを構築している記事が少ないからです
以前, 似ている記事を書いたのですが, かなり説明を端折った箇所があるのと,
ソフトウェア構成がおざなりになっていたため構成など改めてきちんと書いていきます
それとこれは個人的にですがフロントエンドエンジニアなのに
マークアップ案件多すぎて鬱憤晴らしに書いてるフシもあります
前提知識など
- npm や Node.js の環境構築が可能
- Node.js の APIサーバ を構築可能
- Node.js における HTTPリクエスト処理 について理解がある
- 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_token
と oauth_verifier
をサーバに保持します
⑤:保持したrequest_token
とoauth_verifier
を利用し, 認可済みのacceess_token
を取得します
⑥ :最後にクライアントのURLへユーザをリダイレクトさせます
※ 注記
⑤の処理とその後のトークン管理については今回は言及しません
データベースにユーザ名とoauth_verifier
を一緒にハッシュ化してキーにするとかですかね?
気になる方はこちらの記事を読んでみるといいかも?
https://qiita.com/TakahikoKawasaki/items/00f333c72ed96c4da659
私も気になって調べたらよさげな記事があったので(別言語なのと良し悪しに責任はもちませんが...)
実装
まず, 必要なnpmモジュールをインストールし,
npm i http crypto querystring axios
その後, これらのモジュールをインポートします
const http = require('http')
crypto = require('crypto')
qs = require('querystring')
axios = require('axios');
また, 必要な定数をグローバル変数として, 定義しておきます
※ クラスや別ファイルに定義してカプセル化などした方がいいと思いますが, ここでは簡易に
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
を定義します
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
を保持するメソッドを定義します
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
を取得するために利用する,
引数に設定するクエリパラメータを以下に示します
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をハンドリングするほうがいいかもしれません...
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通信を行うためのメソッドです
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
を追加します
その他はの設定は, 自由にカスタマイズしてやってください
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制御など,
必要な処理は各々でカスタムしてみてください
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リクエストの辺りは空デバッグ
※ もしミスなどありましたらよろしくお願いします