mixiアプリとoauth

まずはここからOAuth.phpをダウンロードしてくる。

http://code.google.com/p/oauth/
http://oauth.googlecode.com/svn-history/r526/code/php/OAuth.php

http://developer.mixi.co.jp/appli/spec/pc/require_servers/
mixiの解説ページによるとリビジョン526を指定しているのでそれをダウンロード。

mixiが提供しているコードサンプル

require_once("OAuth.php");

class MixiSignatureMethod extends OAuthSignatureMethod_RSA_SHA1 {
  protected function fetch_public_cert(&$request) {
  return <<< EOD
-----BEGIN CERTIFICATE-----
MIIDfDCCAmSgAwIBAgIJAJU4Z27Mql6HMA0GCSqGSIb3DQEBBQUAMDIxCzAJBgNV
BAYTAkpQMREwDwYDVQQKEwhtaXhpIEluYzEQMA4GA1UEAxMHbWl4aS5qcDAeFw0x
MjAxMDYwMjEyMDNaFw0xNDAxMDUwMjEyMDNaMDIxCzAJBgNVBAYTAkpQMREwDwYD
VQQKEwhtaXhpIEluYzEQMA4GA1UEAxMHbWl4aS5qcDCCASIwDQYJKoZIhvcNAQEB
BQADggEPADCCAQoCggEBAK7r10TPJ6ANhQ8lK7IvX7E8KlWf4hA7LfbSy4EYbQOr
nTsNu52lnqPDnRrMhemWx1cSgOYSzMzW2b5OufSR8rGM/CVQBKmmKScsG2mG0KOX
CdBJKBJv+vk9SV9zfPCLETiE9gqy86Tz9UxLQUKIN/Vwj2GpQ6wIaJQGxrUtkdo9
kRzVA+PrLz+1mdhcNnvu15IzKoqCYwYQDrT1XysvKt8GPkYPlVweBzawpGOBqrPi
zmju7P8yUfqoaO9eJr4arV+dZVup4yGTQlmrHbe/em9+l4HEboD8kiqiUiGwO5Ap
3Ndtco4yfJDYXu4b6rVJAV+c8YZfyboJZqPIEzGTvoECAwEAAaOBlDCBkTAdBgNV
HQ4EFgQUbwJb5soi8EcvovonJ/GS3Di8VngwYgYDVR0jBFswWYAUbwJb5soi8Ecv
ovonJ/GS3Di8VnihNqQ0MDIxCzAJBgNVBAYTAkpQMREwDwYDVQQKEwhtaXhpIElu
YzEQMA4GA1UEAxMHbWl4aS5qcIIJAJU4Z27Mql6HMAwGA1UdEwQFMAMBAf8wDQYJ
KoZIhvcNAQEFBQADggEBAKmheYWIpvyoSz0eBkcmdeZMq+EgH2lrbdfIIkpZ86N9
TFk7whfSlpSsOPaTyJcjTyWOV7orMdsOHwDTdSeCFtIoatHbVsy/KNYQcO+zAI3f
glJ7MjpmN94fv0fRluwzp9g3OL90cC6b4qbDogtttGg8d+jiZ0Y38lOg0EOjjVQp
jocQn0etv9PxY9zzcqOcxJXr/S9GkQFyc4HqzPJwT+FNS44pf7NM5cG9Yr/IKeQh
vhWPsX3bF9Br9o7nSrcTeJzm5u3JQ7Rt0qSX3RPW4XA6uqd4DjrBQqkzk3tc6S25
L1kBVeo29EXDETUnfGz6UuNcxuA6yfXl6l7ypSVpv90=
-----END CERTIFICATE-----
EOD;
  }
}

//Build a request object from the current request
$request = OAuthRequest::from_request(null, null, array_merge($_GET, $_POST));

//Initialize the new signature method
$signature_method = new MixiSignatureMethod();

//Check the request signature
@$signature_valid = $signature_method->check_signature($request, null, null, $_GET["oauth_signature"]);

//Build the output object
$payload = array();
if ($signature_valid == true) {
  $payload["validated"] = "Success! The data was validated";
} else {
  $payload["validated"] = "This request was spoofed";
}

//Add extra parameters to help debugging
$payload["query"] = array_merge($_GET, $_POST);
$payload["rawpost"] = file_get_contents("php://input");

//Return the response as JSON
print(json_encode($payload));

オレが作った検証環境はこんな感じ

.
-- lib
`-- OAuth.php
-- rsa_priv.pem
-- rsa_pub.pem
`-- web |-- oauth.php `-- test.php

検証用の秘密鍵と公開鍵のセットを作成した。

$ openssl genrsa -out rsa_priv.pem 1024
$ openssl rsa -in rsa_priv.pem -out rsa_pub.pem -outform PEM -pubout

webをドキュメントルートとしている。
test.phpの内容はこんな。

<?php
ini_set('display_errors', 'On');
error_reporting(E_ALL);

require_once("../lib/OAuth.php");

class MixiSignatureMethod extends OAuthSignatureMethod_RSA_SHA1 {
  protected function fetch_public_cert(&$request) {
    return file_get_contents('../rsa_pub.pem');
  }

  protected function fetch_private_cert($request) {
    return file_get_contents('../rsa_priv.pem');
  }
}

//Build a request object from the current request
//$request = OAuthRequest::from_request(null, null, array_merge($_GET, $_POST));
$request = OAuthRequest::from_request('GET', 'http://mixi.jp/', array_merge($_GET, $_POST));

//Initialize the new signature method
$signature_method = new MixiSignatureMethod();

//Build signature
$signature = $signature_method->build_signature($request, null, null);
//var_dump($signature);

//Check signature
$ok = $signature_method->check_signature($request, null, null, $signature);
var_dump($ok);

これで何を検証したかというと

まず、mixiのサンプルコードは、
http://developer.mixi.co.jp/appli/spec/mob/validate-oauth-signature/
http://developer.mixi.co.jp/appli/spec/mob/2-legged-oauth/
ここで書かれているように、oauth_signatureという秘密鍵から
oauth仕様に則って生成した値を渡してくる。

では、そのoauth_signatureを自分で作ってみる。

OAuth.phpのどこでその生成処理をしているかというと、
RSA_SHA1を使うので、このクラスの中の、build_signatureメソッドで作られる。

上記のtest.phpでは、build_signatureメソッドで作られた
$signatureをcheck_signatureメソッドに渡して、
その戻り値がtrue or falseで、署名が一致されたかを確認できる。

class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod {/*{{{*/
  public function get_name() {/*{{{*/
    return "RSA-SHA1";
  }/*}}}*/

  protected function fetch_public_cert(&$request) {/*{{{*/
    // not implemented yet, ideas are:
    // (1) do a lookup in a table of trusted certs keyed off of consumer
    // (2) fetch via http using a url provided by the requester
    // (3) some sort of specific discovery code based on request
    //
    // either way should return a string representation of the certificate
    throw Exception("fetch_public_cert not implemented");
  }/*}}}*/

  protected function fetch_private_cert(&$request) {/*{{{*/
    // not implemented yet, ideas are:
    // (1) do a lookup in a table of trusted certs keyed off of consumer
    //
    // either way should return a string representation of the certificate
    throw Exception("fetch_private_cert not implemented");
  }/*}}}*/

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

    // Fetch the private key cert based on the request
    $cert = $this->fetch_private_cert($request);

    //Pull the private key ID from the certificate
    $privatekeyid = openssl_get_privatekey($cert);

    //Check the computer signature against the one passed in the query
    $ok = openssl_sign($base_string, $signature, $privatekeyid);

    //Release the key resource
    openssl_free_key($privatekeyid);

    return base64_encode($signature);
  } /*}}}*/

  public function check_signature(&$request, $consumer, $token, $signature) {/*{{{*/
    $decoded_sig = base64_decode($signature);

    $base_string = $request->get_signature_base_string();

    // Fetch the public key cert based on the request
    $cert = $this->fetch_public_cert($request);

    //Pull the public key ID from the certificate
    $publickeyid = openssl_get_publickey($cert);

    //Check the computer signature against the one passed in the query
    $ok = openssl_verify($base_string, $decoded_sig, $publickeyid);

    //Release the key resource
    openssl_free_key($publickeyid);

    return $ok == 1;
  } /*}}}*/
}/*}}}*/

ブラウザからoauthに必要なパラメータを伴ったアクセスを試す。

mixiの解説ページにあるパラメータ例をそのまま渡してみる。

http://ryouichi31.xxxx.xxx/test.php?oauth_consumer_key=bc906fac81f581c3c96a&oauth_nonce=9dc8fbca0e51842e7449&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1254282755&oauth_version=1.0&opensocial_app_id=123&opensocial_owner_id=xxxxxxxx

これで、$signatureを表示すると、作成された署名の値を確認できる。

$okを表示すると、$signatureの値が公開鍵との一致を試した結果の
true or falseを確認できる。

mixiアプリから外部サーバへoauth認証を行う

上記の仕組みを理解したので、mixiアプリからoauth_signatureを伴ったリクエストを、
自分で構築したサーバのweb/oauth.phpに送信する。

oauth.phpは上記したmixiが提供してるphpスクリプトをそのまま記述しておく。

mixi_reqserver.xmlという名前で、mixiアプリとして登録。

mixi側のキャッシュ、ブラウザのキャッシュ双方を消去して、
ブラウザでこのmixiアプリの動作を確認。

<?xml version="1.0" encoding="UTF-8"?>

<Module>
  <ModulePrefs title="community">
    <Require feature="opensocial-0.8" />
  </ModulePrefs>
  <Content type="html"><![CDATA[

<script type="text/javascript">
function init() {
  var url = "http://ryouichi31.xxxx.xxx/oauth.php";
  var params = {};
  params[gadgets.io.RequestParameters.AUTHORIZATION] = gadgets.io.AuthorizationType.SIGNED;
  gadgets.io.makeRequest(url, function(response) {
    // do something...
    console.log(response);
  }, params);
}
gadgets.util.registerOnLoadHandler(init);
</script>

  ]]></Content>
</Module>

上記のアプリは認証成功の場合、コンソールログのObjectの中に以下のようなデータが入ってる。

data: "{
  "validated":"Success! The data was validated",
  "query":{
  "opensocial_owner_id":"nkib85dcueuyx",
  "opensocial_viewer_id":"nkib85dcueuyx",
  "opensocial_app_id":"37068",
  "opensocial_app_url":
  "http:\/\/ryouichi31.xxxx.xxx\/sample\/mixi_reqserver.xml",
  "oauth_token":"",
  "oauth_consumer_key":"mixi.jp",
  "xoauth_signature_publickey":"sr_20120106",
  "oauth_signature_method":"RSA-SHA1",
  "oauth_nonce":"d101a7e3e2525e7a",
  "oauth_timestamp":"1333077504",
  "oauth_signature":"ItW5\/TnQglzTK9a4bC\/Izz3o9DDTI9oJoLp1vuNwNJJw07ZSZtUQ+K4gkY5UiOpCCCz+6uuu9FtrPIdRYs7YKiSPrRRP2sM9unRsxt6p\/tntjZ\/o+gv4+syJk1aeRy1o+Tj\/B0Wni+OZwuOnCHXC21sYSUhSiJjshDqafvhgWimHFttEmdNYuJuTFBwXILx2saRfbxnFRoUTzpEHIdv5A9m0uIswjOAGegLhroxiG8Qez\/QLqc02N4lZnmWx94wmr5NkFaw7jRs9CE82n5km\/Yb3cVQpHwez\/0zPEElq3uKbkNRikYk6VpD92Msct5hpCg8+jFEtR\/XnVmqUT5llXg=="
  },
  "rawpost":""
}"