LoginSignup
2
2

More than 3 years have passed since last update.

Deno 上の JS で OAuth 認証して Twitter API を使用する

Last updated at Posted at 2020-05-20

Deno が面白そうだったので、試しに Twitter API を使ってみました。

本当は TypeScript を実行できますが、ここでは JavaScript で記述しています.

1. コード

以前自分が書いたコードを少し改良して、さらに Deno 向けに変更を加えました。

参考「ブラウザ上のピュア JavaScript で OAuth 認証して Twitter API を使う

main.js
import Twitter from './twitter.js';

// 
const twitter = new Twitter({
    'api_key'       : '...',
    'api_secret_key': '...',
    'access_token'       : '...',
    'access_token_secret': '...'
});

// 
const json = await twitter.get('friends/list', {
    'screen_name': 'TwitterJP'
});

console.log(json);
twitter.js
import hmacSha1 from './hmac-sha1.js';
import * as base64 from "https://denopkg.com/chiefbiiko/base64/mod.ts";
import * as hex from "https://deno.land/std/encoding/hex.ts";

export default class Twitter {

    #options

    constructor(options) {
        this.#options = options;
    }

    get(path, paramsObj) {
        return this.#request('GET', path, paramsObj);
    }

    async #request(method, path, paramsObj) {

        const url = this.#getRestUrl(path);
        const params = this.#objToArray(paramsObj);

        // 認証情報
        const authHeader = await this.#getAuthHeader(method, url, params);

        const headers = {'Authorization': authHeader};

        // 通信
        const query = this.#percentEncodeParams(params).map(pair => pair.key + '=' + pair.value).join('&');

        const response = await fetch((! params || method === 'POST' ? url : url + '?' + query), {method, headers});

        return response.json();

    }

    async #getAuthHeader(method, url, params) {

        // パラメータ準備
        const oauthParamsObj = {
            'oauth_consumer_key'    : this.#options['api_key'],
            'oauth_nonce'           : this.#getNonce(),
            'oauth_signature_method': 'HMAC-SHA1',
            'oauth_timestamp'       : this.#getTimestamp(),
            'oauth_token'           : this.#options['access_token'],
            'oauth_version'         : '1.0'
        };

        const oauthParams = this.#objToArray(oauthParamsObj);

        const allParams = this.#percentEncodeParams([...oauthParams, ...params]);

        this.#ksort(allParams);

        // シグネチャ作成
        const signature = await this.#getSignature(method, url, allParams);

        // 認証情報
        return 'OAuth ' + this.#percentEncodeParams([...oauthParams, {key: 'oauth_signature', value: signature}]).map(pair => pair.key + '="' + pair.value + '"').join(', ');

    }

    #getSignature(method, url, allParams) {

        const allQuery = allParams.map(pair => pair.key + '=' + pair.value).join('&');

        // シグネチャベース・キー文字列
        const signatureBaseString = [
            method.toUpperCase(),
            this.#percentEncode(url),
            this.#percentEncode(allQuery)
        ].join('&');

        const signatureKeyString = [
            this.#options['api_secret_key'],
            this.#options['access_token_secret']
        ].map(secret => this.#percentEncode(secret)).join('&');

        // シグネチャ計算
        const signatureUint8Array = hmacSha1(signatureBaseString, signatureKeyString);

        return base64.fromUint8Array(signatureUint8Array);

    }

    #getRestUrl(path) {
        return 'https://api.twitter.com/1.1/' + path + '.json';
    }

    /**
     * RFC3986 仕様の encodeURIComponent
     */
    #percentEncode(str) {
        return encodeURIComponent(str).replace(/[!'()*]/g, char => '%' + char.charCodeAt().toString(16));
    }

    #percentEncodeParams(params) {

        return params.map(pair => {
            const key   = this.#percentEncode(pair.key);
            const value = this.#percentEncode(pair.value);
            return {key, value};
        });

    }

    #ksort(params) {

        return params.sort((a, b) => {
            const keyA = a.key;
            const keyB = b.key;
            if ( keyA < keyB ) return -1;
            if ( keyA > keyB ) return 1;
            return 0;
        });

    }

    #getNonce() {
        const array = new Uint8Array(32);
        window.crypto.getRandomValues(array);
        return hex.encodeToString(array);
    }

    #getTimestamp() {
        return Math.floor(Date.now() / 1000);
    }

    #objToArray(object) {
        return Object.entries(object).map(([key, value]) => ({key, value}));
    }

}
hmac-sha1.js
import hmac from 'https://raw.githubusercontent.com/denolibs/hmac/master/lib/mod.ts';
import { Hash, encode } from 'https://deno.land/x/checksum@1.2.0/mod.ts';

const hashSha1 = new Hash('sha1');
const hash = bytes => hashSha1.digest(bytes).data;

const hmacSha1 = (data, key) => hmac(encode(data), encode(key), hash, 64, 20);

export default hmacSha1;

2. Deno の準備

自分は Windows Subsystem for Linux を使用しているので、Linux の方法でインストールしました。

ターミナル
curl -fsSL https://deno.land/x/install/install.sh | sh

環境変数 PATH に deno のインストールディレクトリを追加します。

~/.bash_profile 等に追加
export DENO_INSTALL="/home/<ユーザー名>/.deno"
export PATH="$DENO_INSTALL/bin:$PATH"

参考「Installation - The Deno Manual

3. Deno での実行

Deno で通信をしたい場合には、パーミッションのオプション --allow-net を使用します。

ターミナル
deno run --allow-net main.js

よりセキュアにするために、ホスト名で制限をかけることも可能です。

参考「Permissions - The Deno Manual

4. コードの説明

4.1. Deno での HMAC-SHA1 の計算

Deno では Crypto は実装されていますが SubtleCrypto はありませんので、他の方法で計算します。

Deno 向けの HMAC を計算するライブラリと SHA1 を計算するライブラリがそれぞれあるので、組み合わせたらできました。

参考「GitHub - denolibs/hmac: A faster HMAC module for Deno using TypeScript and WebAssembly
参考「GitHub - manyuanrong/deno-checksum: Sha1、MD5 algorithms for Deno

SHA1 のブロック長は 512 ビット ( = 64 バイト) 、出力長は 160 ビット ( = 20 バイト) です。

参考「SHAシリーズの比較 - SHA-1 - Wikipedia
参考「US Secure Hash Algorithm 1 (SHA1)

4.2. OAuth 認証とシグネチャの計算

別記事にしました。

参考「OAuth 1.0a 認証の実装 (Twitter API 用) - Qiita

参考「Authorizing a request — Twitter Developers
参考「Creating a signature — Twitter Developers

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2