Pearでインストールできるサーバ側のOAuth認証ライブラリを作ってみる

ここ数週間、合間を見てちまちま作ってた。
大体出来た。
パッケージ名は、HTTP_OAuthServer。(暫定)
 

機能

・2legged OAuth認証
・リクエストークン発行。(3legged OAuth)
・ユーザ認可。(3legged OAuth)
・アクセストークン交換。(3legged OAuth)
・保護されたリソースへのアクセス認証。(3legged OAuth)
 
シグネチャ方式はHMAC-SHA1RSA-SHA1
 

目的

今後OAuth認証を使いやすくしたい。
Pearパッケージの作り方を知りたい。
PHP5っぽい書き方の練習。(InterfaceとかAbstractとか)
 

ファイル群

HTTP/OAuthServer.php
HTTP/OAuthServer/Core.php
HTTP/OAuthServer/Signature.php
HTTP/OAuthServer/Util.php
HTTP/OAuthServer/Config.php
HTTP/OAuthServer/Exception.php
 

問題点 他のライブラリと似すぎている

とあるSNSの仕様を是として作ったコンシューマ側のライブラリで認証できるサービスプロバイダ側のライブラリ。
とあるSNSの仕様が本来のOAuthの仕様と若干違ったので、OAuthの公式ドキュメントを読んでそれっぽく変更。
http://code.google.com/p/oauth/ ←これで認証できるように。
 
大体出来て、ちゃんと認証できるかチェックする為に他のライブラリを使ったら、それがやたらと似ていた。
規格に沿ったものを作ろうとしてるんだから当然と言えば当然なんだけど、メソッド名までかぶっているとはこれいかに。
というか、OAuthServerという名前のクラスさえ存在した。
なんてこった。
 
それにしてもこのライブラリ、読んでいて為になる。
コンシューマキーとコンシューマシークレットをひとつのクラスに入れて、それを引数にどうたらこうたら。
プロパティだけ持ってる構造体っぽい使い方。
これまでにメソッドを持たないクラスを宣言したことがなくて、衝撃的だった。
これだったらタイプヒンティングも出来るし、例外処理が楽そう。
C言語とかをやってる人にとっては(言語問わず?)常識なのかもしれないけど。
 

問題点 3legged OAuthはライブラリに含める程のものでもない

もちろんライブラリに含めることは出来るけど、それは使う側に制約を課すに近いような気がする。
制約と言うか、機能的には何の問題もないけど、ちゃんとOAuthについて知ってる人は嫌でもライブラリを書いた人に従わなきゃいけないというか。
データベース連携とブラウザへの出力は使う側が実装するのは決まってることだから、それに従う位なら自分で書いた方がいいというか。
 
IOAuthServerConfig(Interface)を実装したクラスのインスタンスをOAuthServer::setConfig()で入れてデータベース連携させるようにしてる。
どこまでライブラリ内でやって、どこからをConfigでやらせるか。
これがとっても問題。
 

案1 ライブラリ内で全て用意してConfigではDB連携してその結果を返す事しかさせない。

・Configの中でランダムなトークンを生成するメソッドを作る。
・それをOAuthServer内の3legged系メソッドで使う。
・リクエストパラメータとか生成された値とかをデータベースのsetとwhereに見立てた連想配列を2つ用意して、Config内のメソッドに引数2つつけて渡す。
・Configでは渡されたパラメータをほぼそのまま使ってデータベース連携、結果だけをリターン。
 

案2 大体の事はConfigにやらせて、ライブラリでは最低限のことだけをさせる。

・必要最低限のパラメータを引数にConfigのメソッドに渡す。
・Config内でランダム文字列生成。
・Config内でデータベース連携。
・Config内メソッドは生成した文字列をリターン。
 

案1のメリット/デメリット

メリット
・いつ何をすればいいのか知らなくてもなんとなく使える。
・楽。
デメリット
・引数で渡されるパラメータは連想配列なので理解しづらい。
 

案2のメリット/デメリット

メリット
・ソースの書き方に自由度が高い。
・引数がシンプル。
デメリット
・面倒。
・生成したトークンとかを決まった形でリターンしなくちゃいけない。
・ライブラリ側はConfigのラッパーみたいな感じになっちゃう。
・ライブラリ側とConfig側で同じような機能だからメソッド名がややこしい。
・むしろ3legged系のメソッドを用意する必要がない。
 
個人的に、使う側のソースをめちゃくちゃ短くするのが好きで、半実装的な部分さえライブラリに含めてしまう傾向があって、万人向けじゃないかも。
GoogleCodeで公開されてる奴とかでいうところのOAuthServerがこっちでいうOAuthServerCoreで、ライブラリに含まれてない部分がこっちでいうOAuthServer。
個人的には使いやすくはあると思う。
というか、感覚的にはGoogleCodeで公開されてるOAuthServerのラッパークラスがこっちのOAuthServerみたいな。
 

2legged OAuth認証をするデータベースを使わないシンプルな例

Configは部分の仕様は完全に決まったわけじゃない。
3legged OAuth部分は案1。
使ってないけど。
 

<?php
require_once '../../HTTP/OAuthServer.php';

class MyOAuthConfig1 extends OAuthServerConfig
{
    function getValidSecret($consumerKey)
    {
        switch ($consumerKey) {
        case 'example.com':
            return 'examplecompass';
            break;
        case 'example.net':
            return 'examplenetpass';
            break;
        case 'example.org':
            return 'exampleorgpass';
            break;
        }
    }

    function getValidPublicKey($consumerKey)
    {
        switch ($consumerKey) {
        case 'example.com':
            return file_get_contents(dirname(__FILE__).'/examplecom.crt');
            break;
        case 'example.net':
            return file_get_contents(dirname(__FILE__).'/examplenet.crt');
            break;
        case 'example.org':
            return file_get_contents(dirname(__FILE__).'/exampleorg.crt');
            break;
        }
    }

    function isValidTimestamp($timestamp)
    {
        return true;
    }

    public function saveRequestToken($insert)
    {
        return false;
    }

    public function saveUserAuthorization($insert, $where)
    {
        return '';
    }

    public function saveAccessToken($insert, $where)
    {
        return false;
    }

    public function isValidToken($consumerKey,$oauth_token)
    {
        return false;
    }
}

$os = new OAuthServer();
$os->setConfig(new MyOAuthConfig1());

try {
    $os->doAuthenticate2L();
} catch(Exception $e) {
    header('HTTP/1.0 '.$e->getCode());
    echo $e->getMessage();
    exit();
}

var_dump($os->isSuccess());
var_dump($os->getConsumerKey());
print_r($os->getRequestParam());
echo $os->getRequestInfo();

 

3legged OAuth認証 リクエストークン発行のサンプル

これ位短いと気持ちいい。
MyOAuthConfig2はデータベース連携するようにしたConfig。
コンシューマ名とタイムスタンプ、発行したトークンとシークレットをDBに保持。
 

<?php
require_once '../../HTTP/OAuthServer.php';
require_once './example2_config.php';
require_once './mysql_oauthserver.php';

$os = new OAuthServer();
$os->setConfig(new MyOAuthConfig2(new mysql_oauthserver()));

try {
    echo $os->issueRequestToken();
} catch(Exception $e) {
    header('HTTP/1.0 '.$e->getCode());
    echo $e->getMessage();
    exit();
}

 

3legged OAuth認証 ユーザ認可のサンプル

人間様を相手にするからやたらと長くなった。
わざわざSmarty使わなかったのが原因だけど。
ユーザがログインしてなかったらログイン。
認可フォームを表示。
認可したらコールバックURLをDBから引っ張ってきて、ブラウザを遷移させる。
 

<?php
require_once '../../HTTP/OAuthServer.php';
require_once './example2_config.php';
require_once './mysql_oauthserver.php';

session_start();
$req = array_merge($_GET, $_POST);

$db = new mysql_oauthserver();
$os = new OAuthServer();
$os->setConfig(new MyOAuthConfig2($db));

// exec login
if (isset($req['id'], $req['pass'])) {
    $login = $db->select1c("SELECT person_id FROM person WHERE login_id=:id AND login_pass=:pass", $req);
    if ($login) {
        $_SESSION['server']['person_id'] = $login;
    } else {
        $_SESSION['server']['person_id'] = null;
    }
}

// show login form
if (!isset($_SESSION['server']['person_id'])) {
    echo <<< LOGINFORM
    Login<br />
    <form action="" method="post">
    login id : <input type="text" name="id" /><br />
    pass : <input type="text" name="pass" /><br />
    <input type="submit" />
    </form>
LOGINFORM;
    exit();
}

// check oauth_token
if (!isset($req['oauth_token'])) {
    echo "no data";
    exit();
}

// show authorize form
if (!isset($req['csrf'], $_SESSION['server']['csrf']) || $_SESSION['server']['csrf']!==$req['csrf']) {
    $csrf = sha1(mt_rand().microtime());
    $_SESSION['server']['csrf'] = $csrf;
    echo <<< AUTHFORM
    Authorize the consumer?<br />
    <form action="" method="post">
    <input type="hidden" name="oauth_token" value="{$req['oauth_token']}" />
    <input type="hidden" name="csrf" value="{$csrf}" />
    <input type="submit" value="Allow" />
    </form>
AUTHFORM;
    exit();
}

// exec authorize
try {
    $callback = $os->authorizeToken($req['oauth_token']);
    echo $callback;
} catch(Exception $e) {
    header('HTTP/1.0 '.$e->getCode());
    echo $e->getMessage();
}

 

3legged OAuth認証 アクセストークン交換のサンプル

リクエストークンをアクセストークンと交換。
 

<?php
require_once '../../HTTP/OAuthServer.php';
require_once './example2_config.php';
require_once './mysql_oauthserver.php';

$os = new OAuthServer();
$os->setConfig(new MyOAuthConfig2(new mysql_oauthserver()));

try {
    echo $os->exchangeAccessToken();
} catch(Exception $e) {
    header('HTTP/1.0 '.$e->getCode());
    echo $e->getMessage()."\n";
    exit();
}

 

3legged OAuth認証 保護されたリソースへのアクセスのサンプル

oauth_verifierとかでうまい事生成したシグネチャとかを云々。
 

<?php
require_once '../../HTTP/OAuthServer.php';
require_once './example2_config.php';
require_once './mysql_oauthserver.php';

$os = new OAuthServer();
$os->setConfig(new MyOAuthConfig2(new mysql_oauthserver()));

try {
    $os->doAuthenticate3L();
} catch(Exception $e) {
    header('HTTP/1.0 '.$e->getCode());
    echo $e->getMessage();
    exit();
}

var_dump($os->isSuccess());
var_dump($os->getRequestorID());
print_r($os->getRequestParam());
echo $os->getRequestInfo();

 

このライブラリの行く末はいかに

結局使うのは自分自身だから別に自分の使いやすいと思うものでいいんだけど。
公開するのはやぶさかではないけど、メリットがあるのかどうなのか。
需要があるなら、という感じ。
そろそろ「OpenSocial」ってタグよりも「OAuth」ってタグの方が適当になってきたかもしれない。