OAuthについて(OpenIDとは違うのだよ)

OAuth

OpenIDが、あなたは本人ですか?ねぇほんと?ねぇ?的な身元証明の技術であるのに対して、OAuthはこの人に合い鍵渡していい?ねぇ?ほんとにいいの?的な私財へのアクセス許可の技術です。

現在、Twitterの他にもYahooやGoogleのAPIもOAuth認証に対応しています。FlickrもOAuthのベースになってるような認証だった気がするけど、今はどうなんでしょうね。

とりあえずベーシック認証よりは、よっぽどセキュアというわけですが、詳しいメリットや成り立ちなどは、下の参考URLでもご覧くださいまし。

参考:APIアクセス権を委譲するプロトコル、OAuthを知る − @IT

そのプロセス

コンシューマー登録
アパートの管理人(サービスプロバイダ ex.Twitter)に、合い鍵収集業者(コンシューマー)として登録し、業者ID ( コンシューマーキーコンシューマーシークレット ) を得ます。
リクエストトークンの請求
アパートの住人(ユーザー ex.Twitterユーザー)に、合い鍵くれとリクエストを送ります。その際、業者IDをユーザーに渡して、OKなら業者IDを管理人に渡してくれ、と頼みます。
ユーザーの認否
ユーザーは管理人のとこに行って、この業者に合い鍵を渡すかどうかを判断します。(Twitterとの連携サービスとかをやると、良く出てくるアレです。許可・不許可の。)
リクエストトークンの発行
ユーザーがOKを出してくれると、合い鍵業者は、管理人に合い鍵を請求するための委任状(リクエストトークン)をもらえます。
アクセストークンの入手
委任状を手に入れた合い鍵業者は、それを携えて管理人に、そのユーザーの合い鍵(アクセストークン)をもらいに行って、それを受け取ります。
合い鍵が変わったら?
合い鍵はたまに変わってしまうかもしれません。そのとき、合い鍵業者は委任状と前の鍵を持って、もう一度管理人に会いに行きます。委任状が今でも有効なら、新しい合い鍵を受け取れます。すでに無効なら、それは住人が、あなたとの縁を切ったということです。

逆にわかりづらい?

抽象化したら逆に分かりづらい気がする。そうでもないか?まあ、そんな感じでトカゲのしっぽを切れる程度の便利認証でござる。一晩だけの関係とかでもいいわけですよ。合い鍵業者とか訳の分からないことを言わずに、夜這い業者と訳した方が良かったか。

前エントリーで述べたような、HTTPリクエストの実装よりは、よっぽどハマりポイントはありません。仕様に従って作れば一通り問題ありませんでした。OAuthで重要なのは、その意義(使い所)とプロセスを理解することです。

RFC3986の形式でちゃんとエンコードしないとダメとか、そういうのはありますが、コンシューマー側を作る分には、そこまで大変ではありません。サービスプロバイダー側を実装するのは結構面倒くさそうな...。

また普通にソースコードも置いておく

ちょっと古いやつだけど。

$key = 'コンシューマーキー';
$secret = 'コンシューマーシークレット';

$oAuth = new OAuth($key, $secret);

$url = 'APIへのURL';
$params = '連想配列になったパラメーター';
$http_method = 'GETとかPOSTとか';

// これでOAuth用のリクエストを生成します
// アクセストークンとか受け取った後は、
// インスタンス作るときにトークン&トークンシークレットも渡す感じ
$request = $oAuth->buildRequest($url, $params, 'HMAC-SHA1', $http_method);

後日Tiwtter的なラッパーサンプルも出しておきます。最後にOAuth用の本体をば。

<?php

abstract class OAuth_Consumer
{
    abstract protected function _setUrl();
    abstract protected function httpRequest($url, $params = array(), $http_method = 'GET');

    protected function _parseQuery($query)
    {
        if ( empty($query) ) return false;

        $ary    = explode('&', $query);

        if ( !is_array($ary) ) return false;

        foreach ( $ary as $a ) {
            list($key, $val) = explode('=', $a);
            $parsed[$key]  = $val;
        }

        return !empty($parsed) ? $parsed : false;
    }

    protected function getReqToken()
    {
        if ( !!($this->httpRequest($this->request_token_url)) )
        {
            $token  = $this->_parseQuery($this->response->body);
        }
        if ( !empty($token) )
        {
            $this->oAuth = OAuth::RequestToken($this->oAuth, $token['oauth_token'], $token['oauth_token_secret']);
            return $token;
        }
        return $this->response->body;
    }

    protected function getAcsToken()
    {
        if ( !!($this->httpRequest($this->access_token_url)) )
        {
            $token  = $this->_parseQuery($this->response->body);
        }
        if ( !empty($token) )
        {
            $this->oAuth = OAuth::AccessToken($this->oAuth, $token['oauth_token'], $token['oauth_token_secret']);
            return $token;
        }
        return $this->response->body;
    }
}

class OAuth
{
    protected $version = '1.0';
    protected $base;

    public  $key;
    public  $secret;

    public  $token;
    public  $token_secret;

    public  $callback;
    public  $scope;
    public  $verifier;
    public  $handler;
    public  $display;

    public function __construct($key, $secret, $token = null, $token_secret = null)
    {
        $this->key     = $key;
        $this->secret  = $secret;

        if ( !empty($token) && !empty($token_secret) )
        {
            $this->token        = $token;
            $this->token_secret = $token_secret;
        }
        else
        {
            $this->token        = null;
            $this->token_secret = null;
        }
    }

    static public function RequestToken($consumer, $token, $token_secret)
    {
        return new OAuth_RequestToken($consumer, $token, $token_secret);
    }

    static public function AccessToken($consumer, $token, $token_secret)
    {
        return new OAuth_AccessToken($consumer, $token, $token_secret);
    }

    public function setCallback($url)
    {
        $this->callback = $url;
    }

    public function setScope($scope)
    {
        $this->scope    = $scope;
    }

    public function setVerifier($verifier)
    {
        $this->verifier = $verifier;
    }

    public function setHandler($handler)
    {
        $this->handler  = $handler;
    }

    public function setDisplayName($name)
    {
        $this->display  = $name;
    }

    public function mergeOAuthParams($params, $method)
    {
        $params = array_merge((array)$params, array(
                             'oauth_version'      => $this->version,
                             'oauth_nonce'        => md5(microtime().mt_rand()),
                             'oauth_timestamp'    => time(),
                             'oauth_consumer_key' => $this->key,
                             'oauth_signature_method'   => $method,
                             ));

        if ( !empty($this->token) )
            $params['oauth_token'] = $this->token;

        if ( !empty($this->callback) )
            $params['oauth_callback'] = $this->callback;

        if ( !empty($this->scope) )
            $params['scope'] = $this->scope;

        if ( !empty($this->verifier) )
            $params['oauth_verifier'] = $this->verifier;

        if ( !empty($this->handler) )
            $params['oauth_session_handle'] = $this->handler;

        if ( !empty($this->display) )
            $params['xoauth_dispalay_name'] = $this->display;

        return $params;
    }

    public function buildSignature($url, $params, $method, $http_method = 'GET')
    {
        /**
         * build base strings
         */
        $material   = array(
                            OAuth_Signature::rfc3986($http_method),
                            OAuth_Signature::rfc3986($url),
                            OAuth_Signature::rfc3986($this->_HttpBuildQuery($params)),
                            );

        $this->base = implode('&', $material);

        /**
         * detect signature method
         */
        switch ($method)
        {
            case 'HMAC-SHA1' :
                $signature = OAuth_Signature::hmacSha1($this->base, $this->secret, $this->token_secret);
                return $signature;
            default:
                break;
        }
    }

    public function buildRequest($url, $params, $method, $http_method = 'GET')
    {
        $this->params   = $this->mergeOAuthParams($params, $method);

        $signature      = $this->buildSignature($url, $this->params, $method, $http_method);

        $this->params['oauth_signature']    = $signature;

        $query          = $this->_HttpBuildQuery($this->params);

        return $url.'?'.$query;
    }

    public function _HttpBuildQuery($params)
    {
        while ( list($key, $val) = each($params) ) {
            $key    = OAuth_Signature::rfc3986($key);
            $val    = OAuth_Signature::rfc3986($val);
            $encoded[$key] = $val;
        }

        $params = $encoded;
        ksort($params);

        foreach ( $params as $key => $val ) {
            $queries[] = $key.'='.$val;
        }

        return implode('&', $queries);
    }
}

class OAuth_RequestToken extends OAuth
{
    public function __construct($consumer, $token, $token_secret)
    {
        parent::__construct($consumer->key, $consumer->secret, $token, $token_secret);
    }
}

class OAuth_AccessToken extends OAuth
{
    public function __construct($consumer, $token, $token_secret)
    {
        parent::__construct($consumer->key, $consumer->secret, $token, $token_secret);
    }
}

class OAuth_Signature
{
    static public function hmacSha1($base, $consumer_secret, $token_secret = '')
    {
        $keys   = array(
                        OAuth_Signature::rfc3986($consumer_secret),
                        OAuth_Signature::rfc3986($token_secret),
                        );

        $key    = implode('&', $keys);

        return base64_encode(hash_hmac('sha1', $base, $key, true));
    }
   
    static public function rfc3986($str)
    {
        return str_replace('%7E', '~', rawurlencode($str));
    }
   
    static private function alias_hash_hmac()
    {
        // そのうちhash_hmac関数の代替品を組み込む
    }
}
?>