OAuthは調べ尽くし、OAuthを拡張した独自プロトコルも考案して、OAuthには満足した。
だから今度はOpenIDについて調べてみた。
OpenID Enabledとかいう所のライブラリを使うのが主流らしい。
しかしそのライブラリ、めちゃくちゃ汚い。
・とあるクラスを環境に応じてインスタンス化して返すだけのグローバルな関数が大量にある。
=> Class::factory() とかを使えば見やすい。
・関数やクラスがどのファイルに書かれてるか予想が付かない。
=> 是非ともPear準拠のディレクトリ構成にして欲しい。
で、ソースを小一時間読んでたらとってもイライラしてきたから、仕様書読んで自分でライブラリ作る事に。
とりあえず、discoveryとassociationをして、エンドユーザを飛ばす先のURLを生成する所までやってみた。
合ってるかは解らないけど、とりあえず飛んで返ってくるから、なんとなくそれっぽく出来てる気がする。
キャッシュさせた方がいいらしいけど、そこはまだ。
エンドユーザがプロバイダから値付きで帰ってきた時に、キャッシュしてないと先に進めなかったから今日はここまで。
めんどくさいから、キャッシュはMemcacheにでも入れようか。
Diffie-Hellman鍵共有
PearにあったCrypt_DiffieHellmanを使った。
Diffie-Hellman鍵共有とは
Crypt_DiffieHellmanのコメントと、↓この動画を見たらなんとなく解った。
YouTube - Diffie-Hellman鍵交換ってナニ? (part 1/2)
http://www.youtube.com/watch?v=M45Z2a50VDo
YouTube - Diffie-Hellman鍵交換ってナニ? (part 2/2)
http://www.youtube.com/watch?v=DonUjrAGrYM
Diffie-Hellman鍵交換のデフォルト値
仕様書にはこう書いてあった。
これは確認済みの素数で、Diffie-Hellman 鍵交換においてデフォルトのモジュラスとして用いられる。16 進表現では、以下の通りである。 DCF93A0B883972EC0E19989AC5A2CE310E1D37717E8D9571BB7623731866E61E F75A2E27898B057F9891C2E27A639C3F29B60814581CD3B2CA3986D268370557 7D45C2E7E52DC81C7A171876E5CEA74B1448BFDFAF18828EFD2519F14E45E382 6634AF1949E5B535CC829A483B8A76223E5D490A257F05BDFF16F2FB22C583AB
16進数から10進数に変換するならPythonが便利。
$ python Python 2.5.5 (r255:77872, Mar 31 2010, 21:03:05) [GCC 4.1.2 20080704 (Red Hat 4.1.2-46)] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> int('DCF93A0B883972EC0E19989AC5A2CE310E1D37717E8D9571BB7623731866E61EF75A2E27898B057F9891C2E27A639C3F29B60814581CD3B2CA3986D2683705577D45C2E7E52DC81C7A171876E5CEA74B1448BFDFAF18828EFD2519F14E45E3826634AF1949E5B535CC829A483B8A76223E5D490A257F05BDFF16F2FB22C583AB', 16) 155172898181473697471232257763715539915724801966915404479707795314057629378541917580651227423698188993727816152646631438561595825688188889951272158842675419950341258706556549803580104870537681476726513255747040765857479291291572334510643245094715007229621094194349783925984760375594985848253359305585439638443L >>>
XRI
@freeXRIって所でXRI発行してもらえた。
多分、ドメインでいう所のサブドメインみたいな。
@id*eth0jp
@freeXRI
http://www.freexri.com/
ZIGOROuのOpenID Short Clinic:OpenIDとXRI――XRIの基本 - ITmedia エンタープライズ
http://www.itmedia.co.jp/enterprise/articles/0805/23/news007.html
Yadis
HTTPヘッダのX-XRDS-Locationで指定されてるXRDSファイルと、XRI形式のxri.netから返ってくるXRDSファイルのフォーマットがちょっと違った。
よく解らないからとりあえずX-XRDS-Locationだけに対応させた。
この解析方法も合ってるかは不明。
XRI Resolution のメモ - Yet Another Hackadelic
http://d.hatena.ne.jp/ZIGOROu/20080814/1218730541
気になる所
ソース
OpenID.php
<?php require_once('CURL_Request.php'); require_once('Crypt/DiffieHellman.php'); class Yadis { private $_data = array(); public function __construct() { } public function add($type, $uri, $priority=null) { if (!isset($this->_data[$type])) { $this->_data[$type] = array(); } if (isset($priority)) { $this->_data[$type][$priority] = $uri; } else { $this->_data[$type][] = $uri; } } public function get($type) { if (isset($this->_data[$type])) { ksort($this->_data[$type]); foreach ($this->_data[$type] as $uri) { return $uri; } } return null; } public static function parse($xrds_data) { $yadis = new Yadis(); $xrds = new SimpleXMLElement($xrds_data); foreach ($xrds->XRD->Service as $service) { if (isset($service->attributes()->priority)) { $priority = (int)$service->attributes()->priority; $uri = (string)$service->URI; foreach ($service->Type as $type) { $yadis->add((string)$type, $uri, $priority); } } } foreach ($xrds->XRD->Service as $service) { if (!isset($service->attributes()->priority)) { $uri = (string)$service->URI; foreach ($service->Type as $type) { $yadis->add((string)$type, $uri, null); } } } return $yadis; } } class OpenID_Exception extends Exception { private $_detail = null; public function __construct($message, $detail=null, $code=0) { $this->_detail = $detail; parent::__construct($message, $code); } public function getDetail() { return $this->_detail; } } class OpenID_Consumer { // const const DH_DEFAULT_MODULUS = '155172898181473697471232257763715539915724801966915404479707795314057629378541917580651227423698188993727816152646631438561595825688188889951272158842675419950341258706556549803580104870537681476726513255747040765857479291291572334510643245094715007229621094194349783925984760375594985848253359305585439638443'; const DH_DEFAULT_GENERATOR = '2'; // property private $_assoc_type = 'HMAC-SHA1'; private $_session_type = 'DH_SHA1'; // resource private $_dh = null; private $_discovery = null; private $_association = null; private $_op_server = null; /* Setter */ public function setAssocType($type) { if (!in_array($type, array('no-encryption', 'HMAC-SHA1', 'HMAC-SHA256'))) { throw new OpenID_Exception('Not implemented association type: '.$type); } $this->_assoc_type = $type; } public function setSessionType($type) { if (!in_array($type, array(null, 'DH-SHA1', 'DH-SHA256'))) { throw new OpenID_Exception('Not implemented session type: '.$type); } $this->_session_type = $type; } /* Getter */ public function getAssocType() { return $this->_assoc_type; } public function getSessionType() { return $this->_session_type; } public function getAssocAlgo() { if (strpos($this->_assoc_type, 'HMAC')===0) { return substr($this->_assoc_type, 5); } return null; } public function getSessionAlgo() { if (!is_null($this->_session_type)) { return substr($this->_session_type, 3); } return null; } /* Process */ public function discovery($identifier, $return_url=null) { // XRI if (strpos($identifier, 'xri://')===0) { $identifier = substr($identifier, 6); } if (in_array(substr($identifier, 0, 1), array('@', '='))) { $xrds_url = sprintf('http://xri.net/%s?_xrd_r=application/xrds+xml', $identifier); throw new OpenID_Exception('XRI is not implemented.'); // URL } else { // Get XRDS URL if (!preg_match('#^https?://.+#', $identifier)) { $identifier = 'http://'.$identifier; } OpenID_Utils::debug('discovery identifier', $identifier); $curl = new CURL_Request(); $curl->setURL($identifier); $curl->setHeader('Accept', 'application/xrds+xml'); $curl->exec(); $xrds_url = $curl->getHeader()->get('X-XRDS-Location'); if (is_null($xrds_url)) { preg_match_all('/<meta [^>]+>/', $curl->getBody(), $meta_arr); $meta_arr = $meta_arr[0]; foreach ($meta_arr as $meta) { if (preg_match('/http-equiv="X-XRDS-Location"/', $meta)) { $meta_content = array(); if (preg_match('/content="([^"]+)"/', $meta, $meta_content)) { $xrds_url = $meta_content[1]; break; } } } } } if (is_null($xrds_url)) { throw new OpenID_Exception('Not found XRDS URL.'); } OpenID_Utils::debug('discovery xrds url', $xrds_url); // Get XRDS data $curl = new CURL_Request(); $curl->setURL($xrds_url); $curl->exec(); $xrds_data = $curl->getBody(); OpenID_Utils::debug('discovery xrds data', $xrds_data); // Parse XRDS data $yadis = Yadis::parse($xrds_data); $this->_discovery = $yadis; } public function association() { $op_server = null; if ($this->_discovery) { $op_server = $this->_discovery->get('http://specs.openid.net/auth/2.0/server'); } if (is_null($op_server)) { throw new OpenID_Exception('OP Server URL is not set.'); } if (strpos($op_server, 'https://')===false && is_null($this->getSessionAlgo())) { throw new OpenID_Exception('HTTPS or DiffieHellman is required.'); } $this->_op_server = $op_server; // paramater $params = array( 'openid.ns' => 'http://specs.openid.net/auth/2.0', 'openid.mode' => 'associate', 'openid.assoc_type' => $this->_assoc_type, 'openid.session_type' => $this->_session_type ); // diffiehellman parameter if ($this->getSessionAlgo()) { $dh = new Crypt_DiffieHellman(OpenID_Consumer::DH_DEFAULT_MODULUS, OpenID_Consumer::DH_DEFAULT_GENERATOR); $dh->generateKeys(); $dh_params = array( 'openid.dh_consumer_public' => base64_encode($dh->getPublicKey(Crypt_DiffieHellman::BTWOC)), 'openid.dh_modulus' => base64_encode($dh->getPrime(Crypt_DiffieHellman::BTWOC)), 'openid.dh_gen' => base64_encode($dh->getGenerator(Crypt_DiffieHellman::BTWOC)) ); $params = array_merge($params, $dh_params); } ksort($params); OpenID_Utils::debug('association server url', $op_server); OpenID_Utils::debug('association params', $params); // request $curl = new CURL_Request(); $curl->setURL($op_server); $curl->setBody($params); $curl->exec(); $code = $curl->getHeader()->getCode(); $resp = OpenID_Utils::parseKeyValue($curl->getBody()); // response error if ($code!=200 || isset($resp['error'])) { $msg = 'Association error'; if (isset($resp['error'])) { $msg .= ': '.$resp['error']; } throw new OpenID_Exception($msg, $resp); } OpenID_Utils::debug('association response', $resp); // share secret if ($this->getSessionAlgo()) { if (!isset($resp['dh_server_public'], $resp['enc_mac_key'])) { throw new OpenID_Exception('Association error: response error'); } // shared secret $public_key = base64_decode($resp['dh_server_public']); $dh->computeSecretKey($public_key, Crypt_DiffieHellman::BINARY); $shared_secret = $dh->getSharedSecretKey(Crypt_DiffieHellman::BTWOC); $shared_secret_hash = hash($this->getSessionAlgo(), $shared_secret, true); // mac_key $enc_mac_key = base64_decode($resp['enc_mac_key']); $mac_key = ''; $len = strlen($enc_mac_key); for ($i=0; $i<$len; $i++) { $mac_key .= chr(ord($enc_mac_key[$i]) ^ ord($shared_secret_hash[$i])); } $resp['mac_key'] = base64_encode($mac_key); OpenID_Utils::debug('association mac_key', $resp['mac_key']); } $this->_association = $resp; return $resp; } public function getAuthorizationParams($return_to=null, $realm=null) { // default return_to if (is_null($return_to)) { $return_to = OpenID_Utils::getDefaultReturnTo(); } if (is_null($realm)) { $realm = $return_to; } // state less mode params $params = array( 'openid.mode' => 'checkid_setup', 'openid.ns' => 'http://specs.openid.net/auth/2.0', 'openid.return_to' => $return_to, 'openid.realm' => $realm, 'openid.claimed_id' => 'http://specs.openid.net/auth/2.0/identifier_select', 'openid.identity' => 'http://specs.openid.net/auth/2.0/identifier_select' ); // state mode params if (isset($this->_association['assoc_handle'])) { $params['openid.assoc_handle'] = $this->_association['assoc_handle']; } ksort($params); OpenID_Utils::debug('getAuthorizationParam param', $params); return $params; } public function getAuthorizationURL($return_to=null, $realm=null) { $params = $this->getAuthorizationParams($return_to, $realm); return sprintf('%s?%s', $this->_op_server, http_build_query($params)); } public function getAuthorizationForm($return_to=null, $realm=null) { $params = $this->getAuthorizationParams($return_to, $realm); $form = sprintf('<form name="openid_identifier" action="%s" method="POST">'."\n", $this->_op_server); foreach ($params as $key=>$value) { $key = htmlspecialchars($key, ENT_QUOTES); $value = htmlspecialchars($value, ENT_QUOTES); $form .= sprintf('<input type="hidden" name="%s" value="%s" />'."\n", $key, $value); } $form .= '</form>'; return $form; } } class OpenID_Utils { const DEBUG = true; private static $_params = null; public static function parseKeyValue($str) { $lines = preg_split("/(\r\n|\r|\n)/", $str); $result = array(); foreach ($lines as $line) { $kv = explode(':', $line, 2); if (count($kv)==2) { $result[trim($kv[0], ' ')] = trim($kv[1], ' '); } } return $result; } public static function buildKeyValue($arr) { $result = ''; foreach ($arr as $key=>$value) { $result .= sprintf("%s:%s\n", $key, $value); } return $result; } public static function getDefaultReturnTo() { $scheme = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']=='on') ? 'https' : 'http'; $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost'; $port = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : '80'; $path = isset($_SERVER['PHP_SELF']) ? $_SERVER['PHP_SELF'] : '/'; $path = '/'.ltrim($path, '/'); return sprintf('%s://%s:%s%s', $scheme, $host, $port, $path); } public static function getParam($key, $default=null) { if (is_null(self::$_params)) { self::$_params = array_merge($_GET, $_POST); } if (isset(self::$_params[$key])) { return self::$_params[$key]; } return $default; } public static function debug($label, $data) { if (self::DEBUG) { echo "----- $label -----\n"; echo trim(print_r($data, 1), "\r\n")."\n\n"; } } } try { //$assoc_type = 'no-encryption'; //$assoc_type = 'HMAC-SHA1'; $assoc_type = 'HMAC-SHA256'; //$session_type = 'DH-SHA1'; $session_type = 'DH-SHA256'; //$identifier = 'mixi.jp'; $identifier = 'yahoo.co.jp'; //$identifier = 'livedoor.com'; //$identifier = 'xri://=zigorou'; //$identifier = 'xri://@id*eth0jp'; $openid = new OpenID_Consumer(); $openid->setAssocType($assoc_type); $openid->setSessionType($session_type); $openid->discovery($identifier); $openid->association(); echo $openid->getAuthorizationURL('http://localhost/')."\n"; } catch (OpenID_Exception $e) { echo "### OpenID_Exception\n"; echo $e->getMessage()."\n"; print_r($e->getDetail()); } catch (Exception $e) { echo "### Exception\n"; echo $e->getMessage()."\n"; }
CURL_Request.php
HTTPリクエストはこれで。
ずっと倦厭してたけど、今回初めてCURLをまともに使ってみた。
<?php class CURL_Header { private $_protocol = null; private $_code = null; private $_message = null; private $_header = array(); public function __construct($header=null) { if (isset($header)) { $eol = CURL_Request::getEOL($header); $arr = explode($eol, $header); foreach ($arr as $i=>$item) { if (0<strlen($item)) { if ($i==0) { list($this->_protocol, $this->_code, $this->_message) = explode(' ', $item, 3); } else { list($key, $value) = explode(':', $item, 2); $this->_header[trim($key)] = trim($value); } } } } } public function getProtocol() { return $this->_protocol; } public function getCode() { return $this->_code; } public function getMessage() { return $this->_message; } public function get($key=null) { if (is_null($key)) { return $this->_header; } else if (isset($this->_header[$key])) { return $this->_header[$key]; } return null; } } class CURL_Request { private $_ch = null; private $_header = array(); private $_res_info = array(); private $_res_head = array(); private $_res_body = null; public function __construct($url=null, $method=null) { $this->_ch = curl_init($url); if (isset($method)) { $this->setMethod($method); } $this->setTimeout(5); } // set option public function setMethod($method) { $method = strtoupper($method); $this->setopt(CURLOPT_CUSTOMREQUEST, $method); } public function setURL($url) { curl_setopt($this->_ch, CURLOPT_URL, $url); } public function setHeader($key, $value=null) { if (is_array($key)) { $this->_header = array_merge($this->_header, $key); } else { $this->_header[$key] = $value; } } public function setBody($value) { $this->setopt(CURLOPT_POSTFIELDS, $value); } public function setTimeout($value, $ms=false) { if ($ms) { $this->setopt(CURLOPT_TIMEOUT_MS, $value); } else { $this->setopt(CURLOPT_TIMEOUT, $value); } } public function setAuth($username, $password) { $this->setopt(CURLOPT_USERPWD, $username.':'.$password); } public function setPort($port) { $this->setopt(CURLOPT_PORT, $port); } public function setopt($key, $value) { curl_setopt($this->_ch, $key, $value); } // request public function exec() { // init $this->_res_init = array(); $this->_res_head = array(); $this->_res_body = null; // set request headers $this->setopt(CURLOPT_HTTPHEADER, $this->_header); // follow location $this->setopt(CURLOPT_FOLLOWLOCATION, true); $this->setopt(CURLOPT_MAXREDIRS, 5); // get header flag $this->setopt(CURLOPT_HEADER, true); // return response flag $this->setopt(CURLOPT_RETURNTRANSFER, true); $this->setopt(CURLOPT_BINARYTRANSFER, true); // execute request $res = curl_exec($this->_ch); // get info $this->_res_info = curl_getinfo($this->_ch); // error if ($this->_res_info['total_time']==0) { throw new Exception('CURL_Request timeout'); } // parse $continue = 0; for ($i=0; $i<=$this->_res_info['redirect_count']+$continue; $i++) { $eol = CURL_Request::getEOL($res); @list($head, $res) = explode($eol.$eol, $res, 2); if (!isset($head, $res)) { break; } array_unshift($this->_res_head, new CURL_Header($head)); if ($this->getHeader()->getCode()==100) { $continue++; } } $this->_res_body = $res; return array( 'info' => $this->_res_info, 'head' => $this->_res_head, 'body' => $res ); } // response public function getInfo($key=null) { if (is_null($key)) { return $this->_res_info; } if (isset($this->_res_info[$key])) { return $this->_res_info[$key]; } return null; } public function getHeader($i=0) { if (is_null($i)) { return $this->_res_head; } if (isset($this->_res_head[$i])) { return $this->_res_head[$i]; } return null; } public function getBody() { return $this->_res_body; } // util public static function getEOL($data) { $eol = "\r\n"; $index = null; $eol_arr = array("\r\n", "\r", "\n"); foreach ($eol_arr as $eol_tmp) { $index_tmp = strpos($data, $eol_tmp); if ($index_tmp!==false && ($index==null || $index_tmp<$index)) { $eol = $eol_tmp; $index = $index_tmp; } } return $eol; } }
実行結果
$ php OpenID.php ----- discovery identifier ----- http://yahoo.co.jp ----- discovery xrds url ----- http://open.login.yahoo.co.jp/openid20/www.yahoo.co.jp/xrds ----- discovery xrds data ----- <?xml version="1.0" encoding="UTF-8"?> <xrds:XRDS xmlns:xrds="xri://$xrds" xmlns:openid="http://openid.net/xmlns/1.0" xmlns="xri://$xrd*($v*2.0)"> <XRD> <Service priority="0"> <Type>http://specs.openid.net/auth/2.0/server</Type> <Type>http://specs.openid.net/extensions/pape/1.0</Type> <Type>http://openid.net/srv/ax/1.0</Type> <Type>http://specs.openid.net/extensions/ui/1.0/mode/popup</Type> <URI>https://open.login.yahooapis.jp/openid/op/auth</URI> </Service> </XRD> </xrds:XRDS> ----- association server url ----- https://open.login.yahooapis.jp/openid/op/auth ----- association params ----- Array ( [openid.assoc_type] => HMAC-SHA256 [openid.dh_consumer_public] => AMSWp1eIDzYQRLI5tByfWXbbfxgZ+loOpu6gyzlOcM2WfpX0+xlMVuYbr/+XxLR3W5qGsR4yJWHA4DZQDW/xE9g/2RCchQVwr2epG8GulS8Zqg0G4l6ojSKLYB3nNcJIe491ZyVTSt46Z1i+qXxHPoQTsZKG1mjdcF1InZ2fAGag [openid.dh_gen] => Ag== [openid.dh_modulus] => ANz5OguIOXLsDhmYmsWizjEOHTdxfo2Vcbt2I3MYZuYe91ouJ4mLBX+YkcLiemOcPym2CBRYHNOyyjmG0mg3BVd9RcLn5S3IHHoXGHblzqdLFEi/368Ygo79JRnxTkXjgmY0rxlJ5bU1zIKaSDuKdiI+XUkKJX8Fvf8W8vsixYOr [openid.mode] => associate [openid.ns] => http://specs.openid.net/auth/2.0 [openid.session_type] => DH-SHA256 ) ----- association response ----- Array ( [ns] => http://specs.openid.net/auth/2.0 [assoc_handle] => cM.nSuRQ43D2zXPK0iZjkSeddx.WMFX9kVHfi9ZLbKRpA_zIFFtQuY4rOqhZxiAxgLgLirDZBkCEfPaXG6StRGbINdOsygjvAs8E9f62HsMQN4VCIKedWwx8uBS.StyrV9do4XRSIA-- [session_type] => DH-SHA256 [assoc_type] => HMAC-SHA256 [expires_in] => 14400 [enc_mac_key] => 5EMa7vCoZspb3aVBre97wkQAXAgshK5s7rL/vWsWCCE= [dh_server_public] => EVhRMwS5+7FcNRFQLQ5Ukqynl9Xt/KnjjUOYQMHDArqAyfu14sqmtvFDlwxkJ12i1KAuUg9Qk4G5O4ynveGDac3sItVlxwlgZO0tSspUKouZShBQZEM57qGLQ8/xtE2jteUYBmpH6IIucQiZwoiKRodfZkvK2XQyll8Tb0O4A0o= ) ----- association mac_key ----- EsLGhFoU7MzTqmCzQE9+3rx7y7wfOqf4/3mLVOzuv9w= ----- getAuthorizationParam param ----- Array ( [openid.assoc_handle] => cM.nSuRQ43D2zXPK0iZjkSeddx.WMFX9kVHfi9ZLbKRpA_zIFFtQuY4rOqhZxiAxgLgLirDZBkCEfPaXG6StRGbINdOsygjvAs8E9f62HsMQN4VCIKedWwx8uBS.StyrV9do4XRSIA-- [openid.claimed_id] => http://specs.openid.net/auth/2.0/identifier_select [openid.identity] => http://specs.openid.net/auth/2.0/identifier_select [openid.mode] => checkid_setup [openid.ns] => http://specs.openid.net/auth/2.0 [openid.realm] => http://localhost/ [openid.return_to] => http://localhost/ ) https://open.login.yahooapis.jp/openid/op/auth?openid.assoc_handle=cM.nSuRQ43D2zXPK0iZjkSeddx.WMFX9kVHfi9ZLbKRpA_zIFFtQuY4rOqhZxiAxgLgLirDZBkCEfPaXG6StRGbINdOsygjvAs8E9f62HsMQN4VCIKedWwx8uBS.StyrV9do4XRSIA--&openid.claimed_id=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.mode=checkid_setup&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0&openid.realm=http%3A%2F%2Flocalhost%2F&openid.return_to=http%3A%2F%2Flocalhost%2F
参考URL
Final: OpenID Authentication 2.0 - 最終版
http://openid-foundation-japan.github.com/openid-authentication.html
Welcome to OpenID Enabled!
http://openidenabled.com/