by ryou

フロントでTwitterAPIを扱おうとしたことを発端にOAuth1.0の認証に関して調べてた

3連休1日半使ってぐだぐだ調べてたことのまとめをぐだぐだ書く。

ことはじめ

TwitterAPIを使うアプリを作ろうと思い始めて、まぁそれならネイティブよりPhoneGap, Cordovaでハイブリッドアプリかなー、と色々調べたり実際に調査のためのコード書いたりしてて。

で、API扱うときはSecretなトークンは少なくとも隠蔽しとかないとまずいってのは理解しつつも、興味本位でフロントエンドのみでAPI扱えないのかなーと調べてたらこれが結構厄介。

$.ajaxで twitter API から JSONデータを取得する – Qiita

トークンの隠蔽以前に、jQuery使ってAPI呼んでもコールバックをいつもの方法で指定出来ない。これはなんでやー、って色々調べてたら止まらなくなった。コールバックとかとりあえずどうでもいいわ状態。結果、何故か自前でOAuthの認証処理とか書いてた。

とりあえず現状調べたことを覚書程度にまとめ。

OAuth1.0の認証の仕組みに関して

TwitterAPIで採用しているOAuth1.0では、認証作業時のハッシュ生成方法としてHMAC-SHA1を採用している。認証のイメージとしてはこちらの記事(VPN – 共通鍵暗号と公開鍵暗号の違い)が非常にわかりやすかった。

APISecretとAccessTokenSecretを組み合わせたものを共通鍵とし、送信データを元にHMAC-SHA1からのBase64でハッシュ値を生成。生成したハッシュ値と元データを一緒に送り、受信側で再度共通鍵を使用して元データからハッシュ値を生成した結果、送られてきたハッシュ値と一致してたら(お互い同じ共通鍵を使用してるはずだから)認証済みユーザーだよね?って感じっぽい。

Secretキーは絶対に隠蔽しとかないといけないけど、Secretじゃないほうのキー・トークンはそんなに気にしないでもいいのかな。後述するけど、フロント側でAPIを呼ぼうとすると、APIKeyとAccessTokenに関してはOAuth1.0の仕様上生データを送信する必要がある手前、ユーザに見られるのは避けられないし。Secretに関してはハッシュ値生成作業はサーバーを噛ますとかしないとダメそう。

OAuthに関しては、前に調べたことがあって概要くらいは知っていたけど、実際の認証方法に関してはノータッチだったので調べるのが面白かった。

AuthorizationはHeaderでなくても良い

$.ajaxで twitter API から JSONデータを取得する – Qiita

参考にさせていただいたこちらの記事で「なお上記サイトではHeaderにAuthorization属性を付与する必要があるとありましたが、不要なようです。」とありましたが、その理由としてはOAuth1.0仕様(日本語訳)の項目3.5に記載の通り、認証情報は

  • HTTP Authorization ヘッダーフィールド
  • HTTP リクエストエンティティボディー
  • HTTP リクエスト URI クエリー

のいずれかに含めれば良く、今回の場合はリクエストヘッダーに含めない代わりにURIクエリーに含めているのでOKな模様。

なお、この過程で、XMLHttpRequestで指定出来るヘッダーには制限があったことを初めて知った。

HTTP アクセス制御 (CORS) | MDN(項目「アクセス制御シナリオの例」の最初あたり)

(最初なぜかjsonpデータの取得をXMLHttpRequestでやろうとしてた。jsonpはscriptタグ生成してハック的な方法でクロスドメインを解決してるの忘れてた…)

書いたコード

とりあえず検証用に書いたコードを供養がてら上げる。

HTML

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Twitter Api Test</title>

  <script src="http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/hmac-sha1.js"></script>
  <script src="http://crypto-js.googlecode.com/svn/tags/3.0.2/build/components/enc-base64-min.js"></script>
  <script src="twauth.js"></script>
  <script>
    function myCallback(ret) {
      console.log(ret);
    }
    (function(){
      var tokens = {
        'apiKey'     : '',
        'apiSecret'  : '',
        'accessToken': '',
        'tokenSecret': ''
      }
      var tw = new TwAuth(tokens.apiKey, tokens.apiSecret);
      tw.setToken(tokens.accessToken, tokens.tokenSecret);
      tw.callAPI(
        'statuses/user_timeline',
        'GET',
        {
          'screen_name': '@screen_name',
          'count': 10,
          'callback': 'myCallback'
        }
      );
    })();

  </script>
</head>
<body>
</body>
</html>

javascript

var TwAuth = (function(){

  var apiBaseUrl = 'https://api.twitter.com/1.1/';

  var mergeObject = function() {
    var args = Array.prototype.slice.call(arguments),
        len  = args.length,
        ret  = {},
        itm;

    for( var i = 0; i < len ; i++ ){
      var arg = args[i];
      for (itm in arg) {
        if (arg.hasOwnProperty(itm))
          ret[itm] = arg[itm];
      }
    }

    return ret;
  };

  var sortObject = function(object) {
    var sorted = {};
    var array = [];
    for (var key in object) {
        if (object.hasOwnProperty(key)) {
            array.push(key);
        }
    }
    array.sort();
    for (var i = 0; i < array.length; i++) {
        sorted[array[i]] = object[array[i]];
    }
    return sorted;
  };

  var createQuery = function(params) {
    var reqParams = "";
    for (var key in params) {
      if (reqParams.length > 0) reqParams += '&';
      reqParams += encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
    }

    return reqParams;
  };

  /***********************************************************************
  コンストラクタ
  **********************************************************************/
  var TwAuth = function(apiKey, apiSecret) {
    // メンバ変数
    this.tokens = {
      "apiKey"           : apiKey,
      "apiSecret"        : apiSecret,
      "accessToken"      : "",
      "accessTokenSecret": ""
    };
  };


  /***********************************************************************
  メンバ関数
  **********************************************************************/
  var proto = TwAuth.prototype;
  proto._createSignatureKey = function() {
    var signatureKey = encodeURIComponent(this.tokens.apiSecret) + '&' + encodeURIComponent(this.tokens.accessTokenSecret);

    return signatureKey;
  };
  proto._createSignatureData = function(reqUrl, reqMethod, params) {
    reqParams = createQuery(params);
    reqParams = encodeURIComponent(reqParams);

    reqMethod = encodeURIComponent(reqMethod);
    reqUrl    = encodeURIComponent(reqUrl);

    var signatureData = reqMethod + '&' + reqUrl + '&' + reqParams;

    return signatureData;
  };
  proto._createSignature = function(apiUrl, method, apiParams) {
    var signatureKey  = this._createSignatureKey();
    var signatureData = this._createSignatureData(apiUrl, method, apiParams);

    var signature = CryptoJS.HmacSHA1(signatureData, signatureKey).toString(CryptoJS.enc.Base64);

    return signature;
  };
  proto.setToken = function(accessToken, accessTokenSecret) {
    this.tokens.accessToken       = accessToken;
    this.tokens.accessTokenSecret = accessTokenSecret;
  };
  proto.callAPI = function(action, method, params) {
    var apiUrl = apiBaseUrl + action + '.json';

    var date = new Date();
    var milTimestamp = date.getTime();

    var authParams = {
      "oauth_token"           : encodeURIComponent(this.tokens.accessToken),
      "oauth_consumer_key"    : encodeURIComponent(this.tokens.apiKey),
      "oauth_signature_method": encodeURIComponent("HMAC-SHA1"),
      "oauth_timestamp"       : encodeURIComponent(Math.floor(milTimestamp / 1000)),
      "oauth_nonce"           : encodeURIComponent(milTimestamp),
      "oauth_version"         : encodeURIComponent("1.0")
    };

    params = mergeObject(params, authParams);
    params = sortObject(params);

    var signature = this._createSignature(apiUrl, method, params);
    params.oauth_signature = signature;

    var head = document.getElementsByTagName('head')[0];
    var s    = document.createElement('script');
    s.type   = 'text/javascript';
    s.src    = apiUrl + '?' + createQuery(params);
    head.appendChild(s);
  };

  return TwAuth;
})();

以上

どっちかっていうと物を作ること自体より、知識蓄える事の方が断然自分には楽しいのでプライベートだと周辺知識ばっか仕入れて物が全然完成しない。次はTwitterAPIを呼ぶ時になんでjQueryのいつもの方法でコールバック指定出来ないのか調べたい。(アプリそっちのけで)

アプリ作る場合はjavascriptじゃなく、APIの操作に関してはネイティブ側で多分対応することにしそう。

参考サイト