日々是好日

プログラミングについてのあれこれ、ムダ知識など

VSCode 拡張機能開発で OAuth 認証 - 2

力技で Access Token の取得までやってやりました!!ヾ(:3ノシヾ)ノシ

参考 www.iruca21.com

やったこと

  • VSCode 拡張機能はてな API を叩くために、OAuth 認証を実行する
    • Web ブラウザの経由なし、コード上で完結
    • requestモジュールではてなにログイン(Cookie の取得)
    • axios, oauthモジュールで OAuth 認証を実行し Access Token を取得

最初はaxios.postでログイン処理をしようとしましたが、どうしてもうまくいかなかったのでrequestに変えました。axiosでログイン処理する方法募集です。

コード

HatenaBlogCode/HatenaBlogUtil.ts at master · Hide-KC/HatenaBlogCode · GitHub

言語は TypeScript です。非同期処理のコールバックの中でさらに非同期処理をつないでいるので、あほみたいにネストされてます。今後処理ごとに切り分けよう(´・ω・`)

import * as api from './APIValues';
import * as OAuth from 'oauth';
import axios from 'axios';
import * as request from 'request';

export class HatenaBlogUtil {
    startOAuth = async () => {
        const oauth = new OAuth.OAuth(
            'https://www.hatena.com/oauth/initiate',
            'https://www.hatena.com/oauth/token',
            api.COMSUMER_KEY,
            api.COMSUMER_SECRET,
            '1.0',
            'oob',
            'HMAC-SHA1'
        );

        //リクエストトークン取得
        await oauth.getOAuthRequestToken({
            "scope": "read_public,write_public,read_private,write_private"
        }, async (err, request_token, request_token_secret, results) => {
            console.log('===============');
            console.log(err);
            console.log("request token: " + request_token);
            console.log('===============');

            console.log('redirectUrl: https://www.hatena.ne.jp/oauth/authorize?oauth_token=' + request_token);

            //ヘッダーを定義
            const headers = {'Content-Type':'application/json'};

            //オプションを定義
            const options = {
                url: 'https://www.hatena.ne.jp/login',
                method: 'POST',
                headers: headers,
                json: true,
                form: {
                    'name': 'AccountID',
                    'password': 'hogehoge'
                }
            };

            //cookie取得してログイン状態にする
            await request(options, async (error, response, body) => {
                const cookie = response.headers['set-cookie'];
                if (error === null && cookie !== undefined){
                    const _rk = (cookie as string[])[5].match("(rk=.*); domain");
                    if (_rk !== null){
                        const rk = _rk[1];
                        console.log('rk: ' + rk);

                        //リクエストトークン付与してリダイレクト
                        await axios.get('https://www.hatena.ne.jp/oauth/authorize', {
                            params: { oauth_token: request_token },
                            headers: { cookie: rk }
                        }).then(async (results) => {
                            console.log(results);
                            const data = results.data as string;
                            const _rkm = data.match("name=\"rkm\" value=\"(.*)\"");
                            
                            if (_rkm !== null){
                                const rkm = _rkm[1];
                                console.log('rkm: ' + rkm);

                                //oauth_verifierの取得
                                await axios.post('https://www.hatena.ne.jp/oauth/authorize', null, {
                                    params: {
                                        rkm: rkm,
                                        oauth_token: request_token,
                                    },
                                    headers: {
                                        cookie: rk
                                    }
                                }).then(async (results) => {
                                    const data = results.data as string;
                                    const _verifier = data.match("<div class=verifier><pre>(.*)</pre></div>");

                                    if (_verifier !== null){
                                        const verifier = _verifier[1];
                                        console.log('verifier: ' + verifier);

                                        //Access Token の取得
                                        await oauth.getOAuthAccessToken(request_token, request_token_secret, verifier,
                                            (err, accessToken, accessTokenSecret, parsedQueryString) => {
                                                if (err === null){
                                                    console.log(">>>Congraturations!!<<<");
                                                    console.log('AccessToken: ' + accessToken);
                                                    console.log('AccessTokenSecret: ' + accessTokenSecret);
                                                    console.log('ParsedQueryString: ' + parsedQueryString);
                                                } else {
                                                    console.log(err);
                                                }
                                        });
                                    }
                                }).catch((err) => {
                                    console.log(err);
                                });
                            }
                        }).catch((err) => {
                            console.log(err);
                        });
                    }
                } else {
                    console.log(error);
                }
            });
        });
    }

リクエストークンの取得

await oauth.getOAuthRequestToken({
    "scope": "read_public,write_public,read_private,write_private"
}, async (err, request_token, request_token_secret, results) => {
    ...
    console.log('redirectUrl: https://www.hatena.ne.jp/oauth/authorize?oauth_token=' + request_token);

oauth#getOAuthRequestTokenに適切なscopeを渡し、request_tokenを取得します。
次に取得したリクエストークンを連携許可 URL (https://www.hatena.ne.jp/oauth/authorize) に付与してリダイレクトしますが、この際はてなにログインしていないと下記のようなフォームが表示され操作が複雑になってしまいます。

f:id:kcpoipoi:20190119202757p:plain
ブラウザで開いた場合(はてな未ログイン)

仮にログイン状態であれば、下記のように許可・拒否ボタンが表示されるのみで処理が簡単になります。

f:id:kcpoipoi:20190119202729p:plain
ブラウザで開いた場合(はてなログイン済)

このログイン状態の判定は Cookie を見ることで行っているらしく、通常プログラム上から連携許可 URL にリダイレクトすると Cookie が無いので未ログイン状態として扱われてしまいます(詳しいことはわかりません。すみません。)。

そのため、はてなにプログラム上からログインし、headersから Cookie(正確にはその中のrkという値)を取得、そして「許可ボタンを押した」相当の操作を実行します。

はてなログイン処理

//ヘッダーを定義
const headers = {'Content-Type':'application/json'};

//オプションを定義
const options = {
    url: 'https://www.hatena.ne.jp/login',
    method: 'POST',
    headers: headers,
    json: true,
    form: {
        'name': 'AccountID',
        'password': 'hogehoge'
    }
};

//cookie取得してログイン状態にする
await request(options, async (error, response, body) => {
    const cookie = response.headers['set-cookie'];
    if (error === null && cookie !== undefined){
        const _rk = (cookie as string[])[5].match("(rk=.*); domain");
        if (_rk !== null){
            const rk = _rk[1];
            console.log('rk: ' + rk);

次に、取得したrkを連携許可 URL に付与してリクエストします。

連携許可処理(oauth_verifier の取得)

//リクエストトークン付与してリダイレクト
await axios.get('https://www.hatena.ne.jp/oauth/authorize'), {
    params: { oauth_token: request_token },
    headers: { cookie: rk }
}).then(async (results) => {
    console.log(results);
    const data = results.data as string;
    const _rkm = data.match("name=\"rkm\" value=\"(.*)\"");
    
    if (_rkm !== null){
        const rkm = _rkm[1];
        console.log('rkm: ' + rkm);

        //oauth_verifierの取得
        await axios.post('https://www.hatena.ne.jp/oauth/authorize', null, {
            params: {
                rkm: rkm,
                oauth_token: request_token,
            },
            headers: {
                cookie: rk
            }
        }).then(async (results) => {
            const data = results.data as string;
            const _verifier = data.match("<div class=verifier><pre>(.*)</pre></div>");

            if (_verifier !== null){
                const verifier = _verifier[1];
                console.log('verifier: ' + verifier);

まず最初は、axios.getrequest_tokenrkを付与してリクエストします。
すると上に挙げたような許可・拒否ページが表示され、許可するためにrkmという値を取得します。

rkmを取得したら、今度は同じ URL に対してrkmを付与し POST します。
認証がうまくいけば、verifierという値が返ってくるはずです。これを使って、最後に Access Token を取得します。

Access Token の取得

//Access Token の取得
await oauth.getOAuthAccessToken(request_token, request_token_secret, verifier,
    (err, accessToken, accessTokenSecret, parsedQueryString) => {
        if (err === null){
            console.log(">>>Congraturations!!<<<");
            console.log('AccessToken: ' + accessToken);
            console.log('AccessTokenSecret: ' + accessTokenSecret);
            console.log('ParsedQueryString: ' + parsedQueryString);
        } else {
            console.log(err);
        }
});

request_token, request_token_secret, verifierを引数にしてoauth#getOAuthAccessTokenを実行すると、めでたく Access Token が取得できます(問題なければ←)。

f:id:kcpoipoi:20190119180829p:plain:w400
Debug log

所感

TypeScript の文法を理解するのはそんなに苦ではなかったですが、Http リクエストがつらかった……全然わからん。
取得した Access Token / Access Token Secret を保存しておくことで、再認証無しにはてな API を叩くことができます(たぶん。未実装←)

これでやっと拡張機能が開発できるー