PHPを読んだことある人のためのOAuthのSignature解説

今回はOAuthのお話です。
なんとなく、世の中に出回っているライブラリを使わず、OAuthの機能を独自で1から作りこむ人もけっこういるような気がします。
今回はPHPのOAuthライブラリを用いて、HMAC-SHA1のSignatureを作成するときのロジックを見てみます。

仕様 : OAuth Core 1.0 Revision A

■ 前提条件

$params = array(
    "opensocial_app_url" => 'http://r-weblife.sakura.ne.jp/SignedRequest/SignedRequest.xml',
    "opensocial_param_a" => "testValueA",
    "opensocial_param_b" => "testValueB"
        );

■ Normalize Request Parameters

base stringを作る準備から。
OAuthのPHPライブラリとしてよく使われているOAuth.phpでは、get_signable_parametersという関数がこの処理に対応しています。

  public function get_signable_parameters() {/*{{{*/
    // Grab all parameters
    $params = $this->parameters;
		
    // Remove oauth_signature if present #1
    if (isset($params['oauth_signature'])) {
      unset($params['oauth_signature']);
    }
		
    // Urlencode both keys and values #2
    $keys = array_map(array('OAuthUtil', 'urlencodeRFC3986'), array_keys($params));
    $values = array_map(array('OAuthUtil', 'urlencodeRFC3986'), array_values($params));
    $params = array_combine($keys, $values);

    // Sort by keys (natsort) #3
    uksort($params, 'strnatcmp');

    // Generate key=value pairs #4
    $pairs = array();
    foreach ($params as $key=>$value ) {
      if (is_array($value)) {
        // If the value is an array, it's because there are multiple 
        // with the same key, sort them, then add all the pairs
        natsort($value);
        foreach ($value as $v2) {
          $pairs[] = $key . '=' . $v2;
        }
      } else {
        $pairs[] = $key . '=' . $value;
      }
    }
		
    // Return the pairs, concated with &
    return implode('&', $pairs);
  }/*}}}*/

処理前のパラメータ

array(8) {
  ["oauth_version"]=>
  string(3) "1.0"
  ["oauth_nonce"]=>
  string(32) "511df52273863c6fab07d1777d40b6d5"
  ["oauth_timestamp"]=>
  int(1252775234)
  ["oauth_consumer_key"]=>
  string(22) "r-weblife.sakura.ne.jp"
  ["opensocial_app_url"]=>
  string(61) "http://r-weblife.sakura.ne.jp/SignedRequest/SignedRequest.xml"
  ["opensocial_param_a"]=>
  string(10) "testValueA"
  ["opensocial_param_b"]=>
  string(10) "testValueB"
  ["oauth_signature_method"]=>
  string(9) "HMAC-SHA1"
}

#1でoauth_signatureを取り除こうとするが、今回は入っていないので変化なし。
#2でそれぞれのパラメータをURLエンコードします。

array(8) {
  ["oauth_version"]=>
  string(3) "1.0"
  ["oauth_nonce"]=>
  string(32) "511df52273863c6fab07d1777d40b6d5"
  ["oauth_timestamp"]=>
  string(10) "1252775234"
  ["oauth_consumer_key"]=>
  string(22) "r-weblife.sakura.ne.jp"
  ["opensocial_app_url"]=>
  string(71) "http%3A%2F%2Fr-weblife.sakura.ne.jp%2FSignedRequest%2FSignedRequest.xml" // URLエンコードされた
  ["opensocial_param_a"]=>
  string(10) "testValueA"
  ["opensocial_param_b"]=>
  string(10) "testValueB"
  ["oauth_signature_method"]=>
  string(9) "HMAC-SHA1"
}

#3でソートします。
仕様では
"Parameters are sorted by name, using lexicographical byte value ordering."
となっており、ここではuksortにstrnatcmpを指定しています。
http://www.php.net/manual/ja/function.uksort.php
http://jp.php.net/manual/ja/function.strnatcmp.php
順番変わってることがわかります。

array(8) {
  ["oauth_consumer_key"]=>
  string(22) "r-weblife.sakura.ne.jp"
  ["oauth_nonce"]=>
  string(32) "511df52273863c6fab07d1777d40b6d5"
  ["oauth_signature_method"]=>
  string(9) "HMAC-SHA1"
  ["oauth_timestamp"]=>
  string(10) "1252775234"
  ["oauth_version"]=>
  string(3) "1.0"
  ["opensocial_app_url"]=>
  string(71) "http%3A%2F%2Fr-weblife.sakura.ne.jp%2FSignedRequest%2FSignedRequest.xml"
  ["opensocial_param_a"]=>
  string(10) "testValueA"
  ["opensocial_param_b"]=>
  string(10) "testValueB"
}

#4でkey=valueの形にして&でつないでます。

oauth_consumer_key=r-weblife.sakura.ne.jp&oauth_nonce=a181f1e450034443ca6c9e01002f6c2c&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1252762421&oauth_version=1.0&opensocial_app_url=http%3A%2F%2Fr-weblife.sakura.ne.jp%2FSignedRequest%2FSignedRequest.xml&opensocial_param_a=testValueA&opensocial_param_b=testValueB

■ Construct Request URL

対応している関数はこちら。

  public function get_normalized_http_url() {/*{{{*/
    $parts = parse_url($this->http_url);

    $port = (@$parts['port']) ? $parts['port'] : '80';
    $scheme = $parts['scheme'];
    $host = $parts['host'];
    $path = @$parts['path'];

    // #1
    if (($scheme == 'https' && $port != '443')
        || ($scheme == 'http' && $port != '80')) {
      $host = "$host:$port";
    }
    return "$scheme://$host$path";
  }/*}}}*/

#1で"httpリクエストの80番ポート指定"と"httpsリクエストの443番ポート指定"が切られますが、今回の例ではもともとありません。

結局返されたのはこの文字列です。

http://r-weblife.sakura.ne.jp/SignedRequest/example.php

■ Concatenate Request Elements

対応している関数はこちら。

  public function get_signature_base_string() {/*{{{*/
    $parts = array(
      $this->get_normalized_http_method(), // 1
      $this->get_normalized_http_url(), // 2
      $this->get_signable_parameters() // 3
    );

    // #1
    $parts = array_map(array('OAuthUtil', 'urlencodeRFC3986'), $parts);

    // #2
    return implode('&', $parts);
  }/*}}}*/

これも仕様そのままです。

Concatenate Request Elements

The following items MUST be concatenated in order into a single string. Each item is encoded (Parameter Encoding) and separated by an '&' character (ASCII code 38), even if empty.

1. The HTTP request method used to send the request. Value MUST be uppercase, for example: HEAD, GET , POST, etc.
2. The request URL from Section 9.1.2 (Construct Request URL).
3. The normalized request parameters string from Section 9.1.1 (Normalize Request Parameters).

とあるので、それぞれに対応した文字列を取得します。

array(3) {
[0]=>
string(3) "GET"
[1]=>
string(55) "http://r-weblife.sakura.ne.jp/SignedRequest/example.php"
[2]=>
string(315) "oauth_consumer_key=r-weblife.sakura.ne.jp&oauth_nonce=a181f1e450034443ca6c9e01002f6c2c&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1252762421&oauth_version=1.0&opensocial_app_url=http%3A%2F%2Fr-weblife.sakura.ne.jp%2FSignedRequest%2FSignedRequest.xml&opensocial_param_a=testValueA&opensocial_param_b=testValueB"
}

そのあと、#1でParameter Encodingして、#2で&でつないでbase stringの出来上がりです。

GET&http%3A%2F%2Fr-weblife.sakura.ne.jp%2FSignedRequest%2Fexample.php&oauth_consumer_key%3Dr-weblife.sakura.ne.jp%26oauth_nonce%3D511df52273863c6fab07d1777d40b6d5%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1252775234%26oauth_version%3D1.0%26opensocial_app_url%3Dhttp%253A%252F%252Fr-weblife.sakura.ne.jp%252FSignedRequest%252FSignedRequest.xml%26opensocial_param_a%3DtestValueA%26opensocial_param_b%3DtestValueB

■ Generating Signature

HMACのときの仕様はこう書いてあります。

oauth_signature is set to the calculated digest octet string, first base64-encoded per [RFC2045] (Freed, N. and N. Borenstein, “Multipurpose Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies,” .) section 6.8, then URL-encoded per Parameter Encoding (Parameter Encoding).

対応している関数はこちら。

  public function build_signature($request, $consumer, $token) {/*{{{*/
    $base_string = $request->get_signature_base_string();
    $request->base_string = $base_string;

    // #1
    $key_parts = array(
      $consumer->secret,
      ($token) ? $token->secret : ""
    );

    // #2
    $key_parts = array_map(array('OAuthUtil','urlencodeRFC3986'), $key_parts);
    $key = implode('&', $key_parts);

    // #3
    return base64_encode( hash_hmac('sha1', $base_string, $key, true));
  }/*}}}*/

#1でhash_hmacするときの鍵を用意します。基本はConsumerSecretで、3leggedのTokenがあるときはTokenSecretも使います。
#2でConsumerSecretとTokenSecretをURLエンコードした後に&で結合します。

最後に#3でhash_hmacしたbase stringをbase64_encodeしてできあがりですね。

oauth_signature=FLppmbztNWFFFQvQFpR+2OZ3urg=

■ デモ

省略。自分で確かめてください。

■ まとめ

自力でもできますが、世界中の人が使うライブラリあるならそれ使ったほうがいいですね。