_storage = new OpenId_Consumer_Storage_File(); } else { $this->_storage = $storage; } $this->_dumbMode = $dumbMode; } /** * Performs check (with possible user interaction) of OpenID identity. * * This is the first step of OpenID authentication process. * On success the function does not return (it does HTTP redirection to * server and exits). On failure it returns false. * * @param string $id OpenID identity * @param string $returnTo URL to redirect response from server to * @param string $root HTTP URL to identify consumer on server * @param mixed $extensions extension object or array of extensions objects * @return bool */ public function login($id, $returnTo = null, $root = null, $extensions = null) { return $this->_checkId( false, $id, $returnTo, $root, $extensions); } /** * Performs immediate check (without user interaction) of OpenID identity. * * This is the first step of OpenID authentication process. * On success the function does not return (it does HTTP redirection to * server and exits). On failure it returns false. * * @param string $id OpenID identity * @param string $returnTo HTTP URL to redirect response from server to * @param string $root HTTP URL to identify consumer on server * @param mixed $extensions extension object or array of extensions objects * @return bool */ public function check($id, $returnTo=null, $root=null, $extensions = null) { return $this->_checkId( true, $id, $returnTo, $root, $extensions); } /** * Verifies authentication response from OpenID server. * * This is the second step of OpenID authentication process. * The function returns true on successful authentication and false on * failure. * * @param array $params HTTP query data from OpenID server * @param string &$identity this argument is set to end-user's claimed * identifier or OpenID provider local identifier. * @param mixed $extensions extension object or array of extensions objects * @return bool */ public function verify($params, &$identity = "", $extensions = null) { $this->_setError(''); $version = 1.1; if (isset($params['openid_ns']) && $params['openid_ns'] == OpenId::NS_2_0) { $version = 2.0; } if (isset($params["openid_claimed_id"])) { $identity = $params["openid_claimed_id"]; } else if (isset($params["openid_identity"])){ $identity = $params["openid_identity"]; } else { $identity = ""; } if ($version < 2.0 && !isset($params["openid_claimed_id"])) { if (defined('SID')) { if (isset($_SESSION["openid"]["identity"]) && isset($_SESSION["openid"]["claimed_id"]) && $_SESSION["openid"]["identity"] === $identity) { $identity = $_SESSION["openid"]["claimed_id"]; } } else { throw new OpenId_Exception("No session opened"); } } if (empty($params['openid_mode'])) { $this->_setError("Missing openid.mode"); return false; } if (empty($params['openid_return_to'])) { $this->_setError("Missing openid.return_to"); return false; } if (empty($params['openid_signed'])) { $this->_setError("Missing openid.signed"); return false; } if (empty($params['openid_sig'])) { $this->_setError("Missing openid.sig"); return false; } if ($params['openid_mode'] != 'id_res') { $this->_setError("Wrong openid.mode '".$params['openid_mode']."' != 'id_res'"); return false; } if (empty($params['openid_assoc_handle'])) { $this->_setError("Missing openid.assoc_handle"); return false; } if ($params['openid_return_to'] != OpenId::selfUrl()) { /* Ignore query part in openid.return_to */ $pos = strpos($params['openid_return_to'], '?'); if ($pos === false || SUBSTR($params['openid_return_to'], 0 , $pos) != OpenId::selfUrl()) { $this->_setError("Wrong openid.return_to '". $params['openid_return_to']."' != '" . OpenId::selfUrl() ."'"); return false; } } if ($version >= 2.0) { if (empty($params['openid_response_nonce'])) { $this->_setError("Missing openid.response_nonce"); return false; } if (empty($params['openid_op_endpoint'])) { $this->_setError("Missing openid.op_endpoint"); return false; /* OpenID 2.0 (11.3) Checking the Nonce */ } else if (!$this->_storage->isUniqueNonce($params['openid_op_endpoint'], $params['openid_response_nonce'])) { $this->_setError("Duplicate openid.response_nonce"); return false; } } if (!empty($params['openid_invalidate_handle'])) { if ($this->_storage->getAssociationByHandle( $params['openid_invalidate_handle'], $url, $macFunc, $secret, $expires)) { $this->_storage->delAssociation($url); } } if ($this->_storage->getAssociationByHandle( $params['openid_assoc_handle'], $url, $macFunc, $secret, $expires)) { $signed = explode(',', $params['openid_signed']); $data = ''; foreach ($signed as $key) { $data .= $key . ':' . $params['openid_' . strtr($key,'.','_')] . "\n"; } if (base64_decode($params['openid_sig']) == OpenId::hashHmac($macFunc, $data, $secret)) { if (!OpenId_Extension::forAll($extensions, 'parseResponse', $params)) { $this->_setError("Extension::parseResponse failure"); return false; } /* OpenID 2.0 (11.2) Verifying Discovered Information */ if (isset($params['openid_claimed_id'])) { $id = $params['openid_claimed_id']; if (!OpenId::normalize($id)) { $this->_setError("Normalization failed"); return false; } else if (!$this->_discovery($id, $discovered_server, $discovered_version)) { $this->_setError("Discovery failed: " . $this->getError()); return false; } else if ((!empty($params['openid_identity']) && $params["openid_identity"] != $id) || (!empty($params['openid_op_endpoint']) && $params['openid_op_endpoint'] != $discovered_server) || $discovered_version != $version) { $this->_setError("Discovery information verification failed"); return false; } } return true; } $this->_storage->delAssociation($url); $this->_setError("Signature check failed"); return false; } else { /* Use dumb mode */ if (isset($params['openid_claimed_id'])) { $id = $params['openid_claimed_id']; } else if (isset($params['openid_identity'])) { $id = $params['openid_identity']; } else { $this->_setError("Missing openid.claimed_id and openid.identity"); return false; } if (!OpenId::normalize($id)) { $this->_setError("Normalization failed"); return false; } else if (!$this->_discovery($id, $server, $discovered_version)) { $this->_setError("Discovery failed: " . $this->getError()); return false; } /* OpenID 2.0 (11.2) Verifying Discovered Information */ if ((isset($params['openid_identity']) && $params["openid_identity"] != $id) || (isset($params['openid_op_endpoint']) && $params['openid_op_endpoint'] != $server) || $discovered_version != $version) { $this->_setError("Discovery information verification failed"); return false; } $params2 = array(); foreach ($params as $key => $val) { if (strpos($key, 'openid_ns_') === 0) { $key = 'openid.ns.' . substr($key, strlen('openid_ns_')); } else if (strpos($key, 'openid_sreg_') === 0) { $key = 'openid.sreg.' . substr($key, strlen('openid_sreg_')); } else if (strpos($key, 'openid_') === 0) { $key = 'openid.' . substr($key, strlen('openid_')); } $params2[$key] = $val; } $params2['openid.mode'] = 'check_authentication'; $ret = $this->_httpRequest($server, 'POST', $params2, $status); if ($status != 200) { $this->_setError("'Dumb' signature verification HTTP request failed"); return false; } $r = array(); if (is_string($ret)) { foreach(explode("\n", $ret) as $line) { $line = trim($line); if (!empty($line)) { $x = explode(':', $line, 2); if (is_array($x) && count($x) == 2) { list($key, $value) = $x; $r[trim($key)] = trim($value); } } } } $ret = $r; if (!empty($ret['invalidate_handle'])) { if ($this->_storage->getAssociationByHandle( $ret['invalidate_handle'], $url, $macFunc, $secret, $expires)) { $this->_storage->delAssociation($url); } } if (isset($ret['is_valid']) && $ret['is_valid'] == 'true') { if (!OpenId_Extension::forAll($extensions, 'parseResponse', $params)) { $this->_setError("Extension::parseResponse failure"); return false; } return true; } $this->_setError("'Dumb' signature verification failed"); return false; } } /** * Store assiciation in internal chace and external storage * * @param string $url OpenID server url * @param string $handle association handle * @param string $macFunc HMAC function (sha1 or sha256) * @param string $secret shared secret * @param integer $expires expiration UNIX time * @return void */ protected function _addAssociation($url, $handle, $macFunc, $secret, $expires) { $this->_cache[$url] = array($handle, $macFunc, $secret, $expires); return $this->_storage->addAssociation( $url, $handle, $macFunc, $secret, $expires); } /** * Retrive assiciation information for given $url from internal cahce or * external storage * * @param string $url OpenID server url * @param string &$handle association handle * @param string &$macFunc HMAC function (sha1 or sha256) * @param string &$secret shared secret * @param integer &$expires expiration UNIX time * @return void */ protected function _getAssociation($url, &$handle, &$macFunc, &$secret, &$expires) { if (isset($this->_cache[$url])) { $handle = $this->_cache[$url][0]; $macFunc = $this->_cache[$url][1]; $secret = $this->_cache[$url][2]; $expires = $this->_cache[$url][3]; return true; } if ($this->_storage->getAssociation( $url, $handle, $macFunc, $secret, $expires)) { $this->_cache[$url] = array($handle, $macFunc, $secret, $expires); return true; } return false; } /** * Performs HTTP request to given $url using given HTTP $method. * Send additinal query specified by variable/value array, * On success returns HTTP response without headers, false on failure. * * @param string $url OpenID server url * @param string $method HTTP request method 'GET' or 'POST' * @param array $params additional qwery parameters to be passed with * @param int &$staus HTTP status code * request * @return mixed */ protected function _httpRequest($url, $method = 'GET', array $params = array(), &$status = null) { $opts = array( 'http' => array( 'method' => $method, 'timeout' => 15, 'maxredirects'=>4, 'useragent' => 'OpenId', ), ); if ($method == 'POST') { $data = http_build_query($params); $opts['http']['header'] = "Content-type: application/x-www-form-urlencoded\r\n" . "Content-Length: " . strlen($data) . "\r\n"; $opts['http']['content'] = $data; } else { if (!empty($params)) { $url .= '?' . http_build_query($params); } } $context = stream_context_create($opts); $body = file_get_contents($url, 0, $context); $status = 0; foreach ($http_response_header as $header) { $header = trim($header); if (preg_match('!^HTTP/[\d\.x]+ (\d+)!', $header, $match)) { $status = (int) $match[1]; break; } } if ($status == 200 || ($status == 400 && !empty($body))) { return $body; }else{ $this->_setError('Bad HTTP response'); return false; } } /** * Create (or reuse existing) association between OpenID consumer and * OpenID server based on Diffie-Hellman key agreement. Returns true * on success and false on failure. * * @param string $url OpenID server url * @param float $version OpenID protocol version * @param string $priv_key for testing only * @return bool */ protected function _associate($url, $version, $priv_key=null) { /* Check if we already have association in chace or storage */ if ($this->_getAssociation( $url, $handle, $macFunc, $secret, $expires)) { return true; } if ($this->_dumbMode) { /* Use dumb mode */ return true; } $params = array(); if ($version >= 2.0) { $params = array( 'openid.ns' => OpenId::NS_2_0, 'openid.mode' => 'associate', 'openid.assoc_type' => 'HMAC-SHA256', 'openid.session_type' => 'DH-SHA256', ); } else { $params = array( 'openid.mode' => 'associate', 'openid.assoc_type' => 'HMAC-SHA1', 'openid.session_type' => 'DH-SHA1', ); } $dh = OpenId::createDhKey(pack('H*', OpenId::DH_P), pack('H*', OpenId::DH_G), $priv_key); $dh_details = OpenId::getDhKeyDetails($dh); $params['openid.dh_modulus'] = base64_encode( OpenId::btwoc($dh_details['p'])); $params['openid.dh_gen'] = base64_encode( OpenId::btwoc($dh_details['g'])); $params['openid.dh_consumer_public'] = base64_encode( OpenId::btwoc($dh_details['pub_key'])); while(1) { $ret = $this->_httpRequest($url, 'POST', $params, $status); if ($ret === false) { $this->_setError("HTTP request failed"); return false; } $r = array(); $bad_response = false; foreach(explode("\n", $ret) as $line) { $line = trim($line); if (!empty($line)) { $x = explode(':', $line, 2); if (is_array($x) && count($x) == 2) { list($key, $value) = $x; $r[trim($key)] = trim($value); } else { $bad_response = true; } } } if ($bad_response && strpos($ret, 'Unknown session type') !== false) { $r['error_code'] = 'unsupported-type'; } $ret = $r; if (isset($ret['error_code']) && $ret['error_code'] == 'unsupported-type') { if ($params['openid.session_type'] == 'DH-SHA256') { $params['openid.session_type'] = 'DH-SHA1'; $params['openid.assoc_type'] = 'HMAC-SHA1'; } else if ($params['openid.session_type'] == 'DH-SHA1') { $params['openid.session_type'] = 'no-encryption'; } else { $this->_setError("The OpenID service responded with: " . $ret['error_code']); return false; } } else { break; } } if ($status != 200) { $this->_setError("The server responded with status code: " . $status); return false; } if ($version >= 2.0 && isset($ret['ns']) && $ret['ns'] != OpenId::NS_2_0) { $this->_setError("Wrong namespace definition in the server response"); return false; } if (!isset($ret['assoc_handle']) || !isset($ret['expires_in']) || !isset($ret['assoc_type']) || $params['openid.assoc_type'] != $ret['assoc_type']) { if ($params['openid.assoc_type'] != $ret['assoc_type']) { $this->_setError("The returned assoc_type differed from the supplied openid.assoc_type"); } else { $this->_setError("Missing required data from provider (assoc_handle, expires_in, assoc_type are required)"); } return false; } $handle = $ret['assoc_handle']; $expiresIn = $ret['expires_in']; if ($ret['assoc_type'] == 'HMAC-SHA1') { $macFunc = 'sha1'; } else if ($ret['assoc_type'] == 'HMAC-SHA256' && $version >= 2.0) { $macFunc = 'sha256'; } else { $this->_setError("Unsupported assoc_type"); return false; } if ((empty($ret['session_type']) || ($version >= 2.0 && $ret['session_type'] == 'no-encryption')) && isset($ret['mac_key'])) { $secret = base64_decode($ret['mac_key']); } else if (isset($ret['session_type']) && $ret['session_type'] == 'DH-SHA1' && !empty($ret['dh_server_public']) && !empty($ret['enc_mac_key'])) { $dhFunc = 'sha1'; } else if (isset($ret['session_type']) && $ret['session_type'] == 'DH-SHA256' && $version >= 2.0 && !empty($ret['dh_server_public']) && !empty($ret['enc_mac_key'])) { $dhFunc = 'sha256'; } else { $this->_setError("Unsupported session_type"); return false; } if (isset($dhFunc)) { $serverPub = base64_decode($ret['dh_server_public']); $dhSec = OpenId::computeDhSecret($serverPub, $dh); if ($dhSec === false) { $this->_setError("DH secret comutation failed"); return false; } $sec = OpenId::digest($dhFunc, $dhSec); if ($sec === false) { $this->_setError("Could not create digest"); return false; } $secret = $sec ^ base64_decode($ret['enc_mac_key']); } if ($macFunc == 'sha1') { if (OpenId::strlen($secret) != 20) { $this->_setError("The length of the sha1 secret must be 20"); return false; } } else if ($macFunc == 'sha256') { if (OpenId::strlen($secret) != 32) { $this->_setError("The length of the sha256 secret must be 32"); return false; } } $this->_addAssociation( $url, $handle, $macFunc, $secret, time() + $expiresIn); return true; } /** * Performs discovery of identity and finds OpenID URL, OpenID server URL * and OpenID protocol version. Returns true on succees and false on * failure. * * @param string &$id OpenID identity URL * @param string &$server OpenID server URL * @param float &$version OpenID protocol version * @return bool * @todo OpenID 2.0 (7.3) XRI and Yadis discovery */ protected function _discovery(&$id, &$server, &$version) { $realId = $id; if ($this->_storage->getDiscoveryInfo( $id, $realId, $server, $version, $expire)) { $id = $realId; return true; } /* TODO: OpenID 2.0 (7.3) XRI and Yadis discovery */ /* HTML-based discovery */ $response = $this->_httpRequest($id, 'GET', array(), $status); if ($status != 200 || !is_string($response)) { return false; } if (preg_match( '/]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid2.provider[ \t]*[^"\']*\\1[^>]*href=(["\'])([^"\']+)\\2[^>]*\/?>/i', $response, $r)) { $version = 2.0; $server = $r[3]; } else if (preg_match( '/]*href=(["\'])([^"\']+)\\1[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid2.provider[ \t]*[^"\']*\\3[^>]*\/?>/i', $response, $r)) { $version = 2.0; $server = $r[2]; } else if (preg_match( '/]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid.server[ \t]*[^"\']*\\1[^>]*href=(["\'])([^"\']+)\\2[^>]*\/?>/i', $response, $r)) { $version = 1.1; $server = $r[3]; } else if (preg_match( '/]*href=(["\'])([^"\']+)\\1[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid.server[ \t]*[^"\']*\\3[^>]*\/?>/i', $response, $r)) { $version = 1.1; $server = $r[2]; } else { return false; } if ($version >= 2.0) { if (preg_match( '/]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid2.local_id[ \t]*[^"\']*\\1[^>]*href=(["\'])([^"\']+)\\2[^>]*\/?>/i', $response, $r)) { $realId = $r[3]; } else if (preg_match( '/]*href=(["\'])([^"\']+)\\1[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid2.local_id[ \t]*[^"\']*\\3[^>]*\/?>/i', $response, $r)) { $realId = $r[2]; } } else { if (preg_match( '/]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid.delegate[ \t]*[^"\']*\\1[^>]*href=(["\'])([^"\']+)\\2[^>]*\/?>/i', $response, $r)) { $realId = $r[3]; } else if (preg_match( '/]*href=(["\'])([^"\']+)\\1[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid.delegate[ \t]*[^"\']*\\3[^>]*\/?>/i', $response, $r)) { $realId = $r[2]; } } $expire = time() + 60 * 60; $this->_storage->addDiscoveryInfo($id, $realId, $server, $version, $expire); $id = $realId; return true; } /** * Performs check of OpenID identity. * * This is the first step of OpenID authentication process. * On success the function does not return (it does HTTP redirection to * server and exits). On failure it returns false. * * @param bool $immediate enables or disables interaction with user * @param string $id OpenID identity * @param string $returnTo HTTP URL to redirect response from server to * @param string $root HTTP URL to identify consumer on server * @param mixed $extensions extension object or array of extensions objects * @return bool */ protected function _checkId($immediate, $id, $returnTo=null, $root=null, $extensions=null) { $this->_setError(''); if (!OpenId::normalize($id)) { $this->_setError("Normalisation failed"); return false; } $claimedId = $id; if (!$this->_discovery($id, $server, $version)) { $this->_setError("Discovery failed: " . $this->getError()); return false; } if (!$this->_associate($server, $version)) { $this->_setError("Association failed: " . $this->getError()); return false; } if (!$this->_getAssociation( $server, $handle, $macFunc, $secret, $expires)) { /* Use dumb mode */ unset($handle); unset($macFunc); unset($secret); unset($expires); } $params = array(); if ($version >= 2.0) { $params['openid.ns'] = OpenId::NS_2_0; } $params['openid.mode'] = $immediate ? 'checkid_immediate' : 'checkid_setup'; $params['openid.identity'] = $id; $params['openid.claimed_id'] = $claimedId; if ($version <= 2.0) { if (defined('SID')) { $_SESSION["openid"] = array( "identity" => $id, "claimed_id" => $claimedId); } else { throw new OpenId_Exception("No session defined"); } } if (isset($handle)) { $params['openid.assoc_handle'] = $handle; } $params['openid.return_to'] = OpenId::absoluteUrl($returnTo); if (empty($root)) { $root = OpenId::selfUrl(); if ($root[strlen($root)-1] != '/') { $root = dirname($root); } } if ($version >= 2.0) { $params['openid.realm'] = $root; } else { $params['openid.trust_root'] = $root; } if (!OpenId_Extension::forAll($extensions, 'prepareRequest', $params)) { $this->_setError("Extension::prepareRequest failure"); return false; } OpenId::redirect($server, $params); return true; } /** * Saves error message * * @param string $message error message */ protected function _setError($message) { $this->_error = $message; } /** * Returns error message that explains failure of login, check or verify * * @return string */ public function getError() { return $this->_error; } }