Consumer.php 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999
  1. <?php
  2. /**
  3. * Zend Framework
  4. *
  5. * LICENSE
  6. *
  7. * This source file is subject to the new BSD license that is bundled
  8. * with this package in the file LICENSE.txt.
  9. * It is also available through the world-wide-web at this URL:
  10. * http://framework.zend.com/license/new-bsd
  11. * If you did not receive a copy of the license and are unable to
  12. * obtain it through the world-wide-web, please send an email
  13. * to license@zend.com so we can send you a copy immediately.
  14. *
  15. * @category Zend
  16. * @package Zend_OpenId
  17. * @subpackage Zend_OpenId_Consumer
  18. * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
  19. * @license http://framework.zend.com/license/new-bsd New BSD License
  20. * @version $Id: Consumer.php 24593 2012-01-05 20:35:02Z matthew $
  21. */
  22. /**
  23. * @see Zend_OpenId
  24. */
  25. require_once "Zend/OpenId.php";
  26. /**
  27. * @see Zend_OpenId_Extension
  28. */
  29. require_once "Zend/OpenId/Extension.php";
  30. /**
  31. * @see Zend_OpenId_Consumer_Storage
  32. */
  33. require_once "Zend/OpenId/Consumer/Storage.php";
  34. /**
  35. * @see Zend_Http_Client
  36. */
  37. require_once 'Zend/Http/Client.php';
  38. /**
  39. * OpenID consumer implementation
  40. *
  41. * @category Zend
  42. * @package Zend_OpenId
  43. * @subpackage Zend_OpenId_Consumer
  44. * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
  45. * @license http://framework.zend.com/license/new-bsd New BSD License
  46. */
  47. class Zend_OpenId_Consumer
  48. {
  49. /**
  50. * Parameters required for signature
  51. */
  52. protected $_signParams = array('op_endpoint', 'return_to', 'response_nonce', 'assoc_handle');
  53. /**
  54. * Reference to an implementation of storage object
  55. *
  56. * @var Zend_OpenId_Consumer_Storage $_storage
  57. */
  58. protected $_storage = null;
  59. /**
  60. * Enables or disables consumer to use association with server based on
  61. * Diffie-Hellman key agreement
  62. *
  63. * @var Zend_OpenId_Consumer_Storage $_dumbMode
  64. */
  65. protected $_dumbMode = false;
  66. /**
  67. * Internal cache to prevent unnecessary access to storage
  68. *
  69. * @var array $_cache
  70. */
  71. protected $_cache = array();
  72. /**
  73. * HTTP client to make HTTP requests
  74. *
  75. * @var Zend_Http_Client $_httpClient
  76. */
  77. private $_httpClient = null;
  78. /**
  79. * HTTP session to store climed_id between requests
  80. *
  81. * @var Zend_Session_Namespace $_session
  82. */
  83. private $_session = null;
  84. /**
  85. * Last error message for logi, check or verify failure
  86. *
  87. * @var string $_error
  88. */
  89. private $_error = '';
  90. /**
  91. * Constructs a Zend_OpenId_Consumer object with given $storage.
  92. * Enables or disables future association with server based on
  93. * Diffie-Hellman key agreement.
  94. *
  95. * @param Zend_OpenId_Consumer_Storage $storage implementation of custom
  96. * storage object
  97. * @param bool $dumbMode Enables or disables consumer to use association
  98. * with server based on Diffie-Hellman key agreement
  99. */
  100. public function __construct(Zend_OpenId_Consumer_Storage $storage = null,
  101. $dumbMode = false)
  102. {
  103. if ($storage === null) {
  104. require_once "Zend/OpenId/Consumer/Storage/File.php";
  105. $this->_storage = new Zend_OpenId_Consumer_Storage_File();
  106. } else {
  107. $this->_storage = $storage;
  108. }
  109. $this->_dumbMode = $dumbMode;
  110. }
  111. /**
  112. * Performs check (with possible user interaction) of OpenID identity.
  113. *
  114. * This is the first step of OpenID authentication process.
  115. * On success the function does not return (it does HTTP redirection to
  116. * server and exits). On failure it returns false.
  117. *
  118. * @param string $id OpenID identity
  119. * @param string $returnTo URL to redirect response from server to
  120. * @param string $root HTTP URL to identify consumer on server
  121. * @param mixed $extensions extension object or array of extensions objects
  122. * @param Zend_Controller_Response_Abstract $response an optional response
  123. * object to perform HTTP or HTML form redirection
  124. * @return bool
  125. */
  126. public function login($id, $returnTo = null, $root = null, $extensions = null,
  127. Zend_Controller_Response_Abstract $response = null)
  128. {
  129. return $this->_checkId(
  130. false,
  131. $id,
  132. $returnTo,
  133. $root,
  134. $extensions,
  135. $response);
  136. }
  137. /**
  138. * Performs immediate check (without user interaction) of OpenID identity.
  139. *
  140. * This is the first step of OpenID authentication process.
  141. * On success the function does not return (it does HTTP redirection to
  142. * server and exits). On failure it returns false.
  143. *
  144. * @param string $id OpenID identity
  145. * @param string $returnTo HTTP URL to redirect response from server to
  146. * @param string $root HTTP URL to identify consumer on server
  147. * @param mixed $extensions extension object or array of extensions objects
  148. * @param Zend_Controller_Response_Abstract $response an optional response
  149. * object to perform HTTP or HTML form redirection
  150. * @return bool
  151. */
  152. public function check($id, $returnTo=null, $root=null, $extensions = null,
  153. Zend_Controller_Response_Abstract $response = null)
  154. {
  155. return $this->_checkId(
  156. true,
  157. $id,
  158. $returnTo,
  159. $root,
  160. $extensions,
  161. $response);
  162. }
  163. /**
  164. * Verifies authentication response from OpenID server.
  165. *
  166. * This is the second step of OpenID authentication process.
  167. * The function returns true on successful authentication and false on
  168. * failure.
  169. *
  170. * @param array $params HTTP query data from OpenID server
  171. * @param string &$identity this argument is set to end-user's claimed
  172. * identifier or OpenID provider local identifier.
  173. * @param mixed $extensions extension object or array of extensions objects
  174. * @return bool
  175. */
  176. public function verify($params, &$identity = "", $extensions = null)
  177. {
  178. $this->_setError('');
  179. $version = 1.1;
  180. if (isset($params['openid_ns']) &&
  181. $params['openid_ns'] == Zend_OpenId::NS_2_0) {
  182. $version = 2.0;
  183. }
  184. if (isset($params["openid_claimed_id"])) {
  185. $identity = $params["openid_claimed_id"];
  186. } else if (isset($params["openid_identity"])){
  187. $identity = $params["openid_identity"];
  188. } else {
  189. $identity = "";
  190. }
  191. if ($version < 2.0 && !isset($params["openid_claimed_id"])) {
  192. if ($this->_session !== null) {
  193. if ($this->_session->identity === $identity) {
  194. $identity = $this->_session->claimed_id;
  195. }
  196. } else if (defined('SID')) {
  197. if (isset($_SESSION["zend_openid"]["identity"]) &&
  198. isset($_SESSION["zend_openid"]["claimed_id"]) &&
  199. $_SESSION["zend_openid"]["identity"] === $identity) {
  200. $identity = $_SESSION["zend_openid"]["claimed_id"];
  201. }
  202. } else {
  203. require_once "Zend/Session/Namespace.php";
  204. $this->_session = new Zend_Session_Namespace("zend_openid");
  205. if ($this->_session->identity === $identity) {
  206. $identity = $this->_session->claimed_id;
  207. }
  208. }
  209. }
  210. if (empty($params['openid_mode'])) {
  211. $this->_setError("Missing openid.mode");
  212. return false;
  213. }
  214. if (empty($params['openid_return_to'])) {
  215. $this->_setError("Missing openid.return_to");
  216. return false;
  217. }
  218. if (empty($params['openid_signed'])) {
  219. $this->_setError("Missing openid.signed");
  220. return false;
  221. }
  222. if (empty($params['openid_sig'])) {
  223. $this->_setError("Missing openid.sig");
  224. return false;
  225. }
  226. if ($params['openid_mode'] != 'id_res') {
  227. $this->_setError("Wrong openid.mode '".$params['openid_mode']."' != 'id_res'");
  228. return false;
  229. }
  230. if (empty($params['openid_assoc_handle'])) {
  231. $this->_setError("Missing openid.assoc_handle");
  232. return false;
  233. }
  234. if ($params['openid_return_to'] != Zend_OpenId::selfUrl()) {
  235. /* Ignore query part in openid.return_to */
  236. $pos = strpos($params['openid_return_to'], '?');
  237. if ($pos === false ||
  238. SUBSTR($params['openid_return_to'], 0 , $pos) != Zend_OpenId::selfUrl()) {
  239. $this->_setError("Wrong openid.return_to '".
  240. $params['openid_return_to']."' != '" . Zend_OpenId::selfUrl() ."'");
  241. return false;
  242. }
  243. }
  244. if ($version >= 2.0) {
  245. if (empty($params['openid_response_nonce'])) {
  246. $this->_setError("Missing openid.response_nonce");
  247. return false;
  248. }
  249. if (empty($params['openid_op_endpoint'])) {
  250. $this->_setError("Missing openid.op_endpoint");
  251. return false;
  252. /* OpenID 2.0 (11.3) Checking the Nonce */
  253. } else if (!$this->_storage->isUniqueNonce($params['openid_op_endpoint'], $params['openid_response_nonce'])) {
  254. $this->_setError("Duplicate openid.response_nonce");
  255. return false;
  256. }
  257. }
  258. if (!empty($params['openid_invalidate_handle'])) {
  259. if ($this->_storage->getAssociationByHandle(
  260. $params['openid_invalidate_handle'],
  261. $url,
  262. $macFunc,
  263. $secret,
  264. $expires)) {
  265. $this->_storage->delAssociation($url);
  266. }
  267. }
  268. if ($this->_storage->getAssociationByHandle(
  269. $params['openid_assoc_handle'],
  270. $url,
  271. $macFunc,
  272. $secret,
  273. $expires)) {
  274. // Security fix - check the association bewteen op_endpoint and assoc_handle
  275. if (isset($params['openid_op_endpoint']) && $url !== $params['openid_op_endpoint']) {
  276. $this->_setError("The op_endpoint URI is not the same of URI associated with the assoc_handle");
  277. return false;
  278. }
  279. $signed = explode(',', $params['openid_signed']);
  280. // Check the parameters for the signature
  281. // @see https://openid.net/specs/openid-authentication-2_0.html#positive_assertions
  282. $toCheck = $this->_signParams;
  283. if (isset($params['openid_claimed_id']) && isset($params['openid_identity'])) {
  284. $toCheck = array_merge($toCheck, array('claimed_id', 'identity'));
  285. }
  286. foreach ($toCheck as $param) {
  287. if (!in_array($param, $signed, true)) {
  288. $this->_setError("The required parameter $param is missing in the signed");
  289. return false;
  290. }
  291. }
  292. $data = '';
  293. foreach ($signed as $key) {
  294. $data .= $key . ':' . $params['openid_' . strtr($key,'.','_')] . "\n";
  295. }
  296. if (base64_decode($params['openid_sig']) ==
  297. Zend_OpenId::hashHmac($macFunc, $data, $secret)) {
  298. if (!Zend_OpenId_Extension::forAll($extensions, 'parseResponse', $params)) {
  299. $this->_setError("Extension::parseResponse failure");
  300. return false;
  301. }
  302. /* OpenID 2.0 (11.2) Verifying Discovered Information */
  303. if (isset($params['openid_claimed_id'])) {
  304. $id = $params['openid_claimed_id'];
  305. if (!Zend_OpenId::normalize($id)) {
  306. $this->_setError("Normalization failed");
  307. return false;
  308. } else if (!$this->_discovery($id, $discovered_server, $discovered_version)) {
  309. $this->_setError("Discovery failed: " . $this->getError());
  310. return false;
  311. } else if ((!empty($params['openid_identity']) &&
  312. $params["openid_identity"] != $id) ||
  313. (!empty($params['openid_op_endpoint']) &&
  314. $params['openid_op_endpoint'] != $discovered_server) ||
  315. $discovered_version != $version) {
  316. $this->_setError("Discovery information verification failed");
  317. return false;
  318. }
  319. }
  320. return true;
  321. }
  322. $this->_storage->delAssociation($url);
  323. $this->_setError("Signature check failed");
  324. return false;
  325. }
  326. else
  327. {
  328. /* Use dumb mode */
  329. if (isset($params['openid_claimed_id'])) {
  330. $id = $params['openid_claimed_id'];
  331. } else if (isset($params['openid_identity'])) {
  332. $id = $params['openid_identity'];
  333. } else {
  334. $this->_setError("Missing openid.claimed_id and openid.identity");
  335. return false;
  336. }
  337. if (!Zend_OpenId::normalize($id)) {
  338. $this->_setError("Normalization failed");
  339. return false;
  340. } else if (!$this->_discovery($id, $server, $discovered_version)) {
  341. $this->_setError("Discovery failed: " . $this->getError());
  342. return false;
  343. }
  344. /* OpenID 2.0 (11.2) Verifying Discovered Information */
  345. if ((isset($params['openid_identity']) &&
  346. $params["openid_identity"] != $id) ||
  347. (isset($params['openid_op_endpoint']) &&
  348. $params['openid_op_endpoint'] != $server) ||
  349. $discovered_version != $version) {
  350. $this->_setError("Discovery information verification failed");
  351. return false;
  352. }
  353. $params2 = array();
  354. foreach ($params as $key => $val) {
  355. if (strpos($key, 'openid_ns_') === 0) {
  356. $key = 'openid.ns.' . substr($key, strlen('openid_ns_'));
  357. } else if (strpos($key, 'openid_sreg_') === 0) {
  358. $key = 'openid.sreg.' . substr($key, strlen('openid_sreg_'));
  359. } else if (strpos($key, 'openid_') === 0) {
  360. $key = 'openid.' . substr($key, strlen('openid_'));
  361. }
  362. $params2[$key] = $val;
  363. }
  364. $params2['openid.mode'] = 'check_authentication';
  365. $ret = $this->_httpRequest($server, 'POST', $params2, $status);
  366. if ($status != 200) {
  367. $this->_setError("'Dumb' signature verification HTTP request failed");
  368. return false;
  369. }
  370. $r = array();
  371. if (is_string($ret)) {
  372. foreach(explode("\n", $ret) as $line) {
  373. $line = trim($line);
  374. if (!empty($line)) {
  375. $x = explode(':', $line, 2);
  376. if (is_array($x) && count($x) == 2) {
  377. list($key, $value) = $x;
  378. $r[trim($key)] = trim($value);
  379. }
  380. }
  381. }
  382. }
  383. $ret = $r;
  384. if (!empty($ret['invalidate_handle'])) {
  385. if ($this->_storage->getAssociationByHandle(
  386. $ret['invalidate_handle'],
  387. $url,
  388. $macFunc,
  389. $secret,
  390. $expires)) {
  391. $this->_storage->delAssociation($url);
  392. }
  393. }
  394. if (isset($ret['is_valid']) && $ret['is_valid'] == 'true') {
  395. if (!Zend_OpenId_Extension::forAll($extensions, 'parseResponse', $params)) {
  396. $this->_setError("Extension::parseResponse failure");
  397. return false;
  398. }
  399. return true;
  400. }
  401. $this->_setError("'Dumb' signature verification failed");
  402. return false;
  403. }
  404. }
  405. /**
  406. * Store assiciation in internal chace and external storage
  407. *
  408. * @param string $url OpenID server url
  409. * @param string $handle association handle
  410. * @param string $macFunc HMAC function (sha1 or sha256)
  411. * @param string $secret shared secret
  412. * @param integer $expires expiration UNIX time
  413. * @return void
  414. */
  415. protected function _addAssociation($url, $handle, $macFunc, $secret, $expires)
  416. {
  417. $this->_cache[$url] = array($handle, $macFunc, $secret, $expires);
  418. return $this->_storage->addAssociation(
  419. $url,
  420. $handle,
  421. $macFunc,
  422. $secret,
  423. $expires);
  424. }
  425. /**
  426. * Retrive assiciation information for given $url from internal cahce or
  427. * external storage
  428. *
  429. * @param string $url OpenID server url
  430. * @param string &$handle association handle
  431. * @param string &$macFunc HMAC function (sha1 or sha256)
  432. * @param string &$secret shared secret
  433. * @param integer &$expires expiration UNIX time
  434. * @return void
  435. */
  436. protected function _getAssociation($url, &$handle, &$macFunc, &$secret, &$expires)
  437. {
  438. if (isset($this->_cache[$url])) {
  439. $handle = $this->_cache[$url][0];
  440. $macFunc = $this->_cache[$url][1];
  441. $secret = $this->_cache[$url][2];
  442. $expires = $this->_cache[$url][3];
  443. return true;
  444. }
  445. if ($this->_storage->getAssociation(
  446. $url,
  447. $handle,
  448. $macFunc,
  449. $secret,
  450. $expires)) {
  451. $this->_cache[$url] = array($handle, $macFunc, $secret, $expires);
  452. return true;
  453. }
  454. return false;
  455. }
  456. /**
  457. * Performs HTTP request to given $url using given HTTP $method.
  458. * Send additinal query specified by variable/value array,
  459. * On success returns HTTP response without headers, false on failure.
  460. *
  461. * @param string $url OpenID server url
  462. * @param string $method HTTP request method 'GET' or 'POST'
  463. * @param array $params additional qwery parameters to be passed with
  464. * @param int &$staus HTTP status code
  465. * request
  466. * @return mixed
  467. */
  468. protected function _httpRequest($url, $method = 'GET', array $params = array(), &$status = null)
  469. {
  470. $client = $this->_httpClient;
  471. if ($client === null) {
  472. $client = new Zend_Http_Client(
  473. $url,
  474. array(
  475. 'maxredirects' => 4,
  476. 'timeout' => 15,
  477. 'useragent' => 'Zend_OpenId'
  478. )
  479. );
  480. } else {
  481. $client->setUri($url);
  482. }
  483. $client->resetParameters();
  484. if ($method == 'POST') {
  485. $client->setMethod(Zend_Http_Client::POST);
  486. $client->setParameterPost($params);
  487. } else {
  488. $client->setMethod(Zend_Http_Client::GET);
  489. $client->setParameterGet($params);
  490. }
  491. try {
  492. $response = $client->request();
  493. } catch (Exception $e) {
  494. $this->_setError('HTTP Request failed: ' . $e->getMessage());
  495. return false;
  496. }
  497. $status = $response->getStatus();
  498. $body = $response->getBody();
  499. if ($status == 200 || ($status == 400 && !empty($body))) {
  500. return $body;
  501. }else{
  502. $this->_setError('Bad HTTP response');
  503. return false;
  504. }
  505. }
  506. /**
  507. * Create (or reuse existing) association between OpenID consumer and
  508. * OpenID server based on Diffie-Hellman key agreement. Returns true
  509. * on success and false on failure.
  510. *
  511. * @param string $url OpenID server url
  512. * @param float $version OpenID protocol version
  513. * @param string $priv_key for testing only
  514. * @return bool
  515. */
  516. protected function _associate($url, $version, $priv_key=null)
  517. {
  518. /* Check if we already have association in chace or storage */
  519. if ($this->_getAssociation(
  520. $url,
  521. $handle,
  522. $macFunc,
  523. $secret,
  524. $expires)) {
  525. return true;
  526. }
  527. if ($this->_dumbMode) {
  528. /* Use dumb mode */
  529. return true;
  530. }
  531. $params = array();
  532. if ($version >= 2.0) {
  533. $params = array(
  534. 'openid.ns' => Zend_OpenId::NS_2_0,
  535. 'openid.mode' => 'associate',
  536. 'openid.assoc_type' => 'HMAC-SHA256',
  537. 'openid.session_type' => 'DH-SHA256',
  538. );
  539. } else {
  540. $params = array(
  541. 'openid.mode' => 'associate',
  542. 'openid.assoc_type' => 'HMAC-SHA1',
  543. 'openid.session_type' => 'DH-SHA1',
  544. );
  545. }
  546. $dh = Zend_OpenId::createDhKey(pack('H*', Zend_OpenId::DH_P),
  547. pack('H*', Zend_OpenId::DH_G),
  548. $priv_key);
  549. $dh_details = Zend_OpenId::getDhKeyDetails($dh);
  550. $params['openid.dh_modulus'] = base64_encode(
  551. Zend_OpenId::btwoc($dh_details['p']));
  552. $params['openid.dh_gen'] = base64_encode(
  553. Zend_OpenId::btwoc($dh_details['g']));
  554. $params['openid.dh_consumer_public'] = base64_encode(
  555. Zend_OpenId::btwoc($dh_details['pub_key']));
  556. while(1) {
  557. $ret = $this->_httpRequest($url, 'POST', $params, $status);
  558. if ($ret === false) {
  559. $this->_setError("HTTP request failed");
  560. return false;
  561. }
  562. $r = array();
  563. $bad_response = false;
  564. foreach(explode("\n", $ret) as $line) {
  565. $line = trim($line);
  566. if (!empty($line)) {
  567. $x = explode(':', $line, 2);
  568. if (is_array($x) && count($x) == 2) {
  569. list($key, $value) = $x;
  570. $r[trim($key)] = trim($value);
  571. } else {
  572. $bad_response = true;
  573. }
  574. }
  575. }
  576. if ($bad_response && strpos($ret, 'Unknown session type') !== false) {
  577. $r['error_code'] = 'unsupported-type';
  578. }
  579. $ret = $r;
  580. if (isset($ret['error_code']) &&
  581. $ret['error_code'] == 'unsupported-type') {
  582. if ($params['openid.session_type'] == 'DH-SHA256') {
  583. $params['openid.session_type'] = 'DH-SHA1';
  584. $params['openid.assoc_type'] = 'HMAC-SHA1';
  585. } else if ($params['openid.session_type'] == 'DH-SHA1') {
  586. $params['openid.session_type'] = 'no-encryption';
  587. } else {
  588. $this->_setError("The OpenID service responded with: " . $ret['error_code']);
  589. return false;
  590. }
  591. } else {
  592. break;
  593. }
  594. }
  595. if ($status != 200) {
  596. $this->_setError("The server responded with status code: " . $status);
  597. return false;
  598. }
  599. if ($version >= 2.0 &&
  600. isset($ret['ns']) &&
  601. $ret['ns'] != Zend_OpenId::NS_2_0) {
  602. $this->_setError("Wrong namespace definition in the server response");
  603. return false;
  604. }
  605. if (!isset($ret['assoc_handle']) ||
  606. !isset($ret['expires_in']) ||
  607. !isset($ret['assoc_type']) ||
  608. $params['openid.assoc_type'] != $ret['assoc_type']) {
  609. if ($params['openid.assoc_type'] != $ret['assoc_type']) {
  610. $this->_setError("The returned assoc_type differed from the supplied openid.assoc_type");
  611. } else {
  612. $this->_setError("Missing required data from provider (assoc_handle, expires_in, assoc_type are required)");
  613. }
  614. return false;
  615. }
  616. $handle = $ret['assoc_handle'];
  617. $expiresIn = $ret['expires_in'];
  618. if ($ret['assoc_type'] == 'HMAC-SHA1') {
  619. $macFunc = 'sha1';
  620. } else if ($ret['assoc_type'] == 'HMAC-SHA256' &&
  621. $version >= 2.0) {
  622. $macFunc = 'sha256';
  623. } else {
  624. $this->_setError("Unsupported assoc_type");
  625. return false;
  626. }
  627. if ((empty($ret['session_type']) ||
  628. ($version >= 2.0 && $ret['session_type'] == 'no-encryption')) &&
  629. isset($ret['mac_key'])) {
  630. $secret = base64_decode($ret['mac_key']);
  631. } else if (isset($ret['session_type']) &&
  632. $ret['session_type'] == 'DH-SHA1' &&
  633. !empty($ret['dh_server_public']) &&
  634. !empty($ret['enc_mac_key'])) {
  635. $dhFunc = 'sha1';
  636. } else if (isset($ret['session_type']) &&
  637. $ret['session_type'] == 'DH-SHA256' &&
  638. $version >= 2.0 &&
  639. !empty($ret['dh_server_public']) &&
  640. !empty($ret['enc_mac_key'])) {
  641. $dhFunc = 'sha256';
  642. } else {
  643. $this->_setError("Unsupported session_type");
  644. return false;
  645. }
  646. if (isset($dhFunc)) {
  647. $serverPub = base64_decode($ret['dh_server_public']);
  648. $dhSec = Zend_OpenId::computeDhSecret($serverPub, $dh);
  649. if ($dhSec === false) {
  650. $this->_setError("DH secret comutation failed");
  651. return false;
  652. }
  653. $sec = Zend_OpenId::digest($dhFunc, $dhSec);
  654. if ($sec === false) {
  655. $this->_setError("Could not create digest");
  656. return false;
  657. }
  658. $secret = $sec ^ base64_decode($ret['enc_mac_key']);
  659. }
  660. if ($macFunc == 'sha1') {
  661. if (Zend_OpenId::strlen($secret) != 20) {
  662. $this->_setError("The length of the sha1 secret must be 20");
  663. return false;
  664. }
  665. } else if ($macFunc == 'sha256') {
  666. if (Zend_OpenId::strlen($secret) != 32) {
  667. $this->_setError("The length of the sha256 secret must be 32");
  668. return false;
  669. }
  670. }
  671. $this->_addAssociation(
  672. $url,
  673. $handle,
  674. $macFunc,
  675. $secret,
  676. time() + $expiresIn);
  677. return true;
  678. }
  679. /**
  680. * Performs discovery of identity and finds OpenID URL, OpenID server URL
  681. * and OpenID protocol version. Returns true on succees and false on
  682. * failure.
  683. *
  684. * @param string &$id OpenID identity URL
  685. * @param string &$server OpenID server URL
  686. * @param float &$version OpenID protocol version
  687. * @return bool
  688. * @todo OpenID 2.0 (7.3) XRI and Yadis discovery
  689. */
  690. protected function _discovery(&$id, &$server, &$version)
  691. {
  692. $realId = $id;
  693. if ($this->_storage->getDiscoveryInfo(
  694. $id,
  695. $realId,
  696. $server,
  697. $version,
  698. $expire)) {
  699. $id = $realId;
  700. return true;
  701. }
  702. $response = $this->_httpRequest($id, 'GET', array(), $status);
  703. if ($status != 200 || !is_string($response)) {
  704. return false;
  705. }
  706. /* OpenID 2.0 (7.3) XRI and Yadis discovery */
  707. if (preg_match(
  708. '/<meta[^>]*http-equiv=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?X-XRDS-Location[ \t]*[^"\']*\\1[^>]*content=(["\'])([^"\']+)\\2[^>]*\/?>/i',
  709. $response,
  710. $r)) {
  711. $XRDS = $r[3];
  712. $version = 2.0;
  713. $response = $this->_httpRequest($XRDS);
  714. if (preg_match(
  715. '/<URI>([^\t]*)<\/URI>/i',
  716. $response,
  717. $x)) {
  718. $server = $x[1];
  719. // $realId
  720. $realId = 'http://specs.openid.net/auth/2.0/identifier_select';
  721. }
  722. else {
  723. $this->_setError("Unable to get URI for XRDS discovery");
  724. }
  725. }
  726. /* HTML-based discovery */
  727. else if (preg_match(
  728. '/<link[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid2.provider[ \t]*[^"\']*\\1[^>]*href=(["\'])([^"\']+)\\2[^>]*\/?>/i',
  729. $response,
  730. $r)) {
  731. $version = 2.0;
  732. $server = $r[3];
  733. } else if (preg_match(
  734. '/<link[^>]*href=(["\'])([^"\']+)\\1[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid2.provider[ \t]*[^"\']*\\3[^>]*\/?>/i',
  735. $response,
  736. $r)) {
  737. $version = 2.0;
  738. $server = $r[2];
  739. } else if (preg_match(
  740. '/<link[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid.server[ \t]*[^"\']*\\1[^>]*href=(["\'])([^"\']+)\\2[^>]*\/?>/i',
  741. $response,
  742. $r)) {
  743. $version = 1.1;
  744. $server = $r[3];
  745. } else if (preg_match(
  746. '/<link[^>]*href=(["\'])([^"\']+)\\1[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid.server[ \t]*[^"\']*\\3[^>]*\/?>/i',
  747. $response,
  748. $r)) {
  749. $version = 1.1;
  750. $server = $r[2];
  751. } else {
  752. return false;
  753. }
  754. if ($version >= 2.0) {
  755. if (preg_match(
  756. '/<link[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid2.local_id[ \t]*[^"\']*\\1[^>]*href=(["\'])([^"\']+)\\2[^>]*\/?>/i',
  757. $response,
  758. $r)) {
  759. $realId = $r[3];
  760. } else if (preg_match(
  761. '/<link[^>]*href=(["\'])([^"\']+)\\1[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid2.local_id[ \t]*[^"\']*\\3[^>]*\/?>/i',
  762. $response,
  763. $r)) {
  764. $realId = $r[2];
  765. }
  766. } else {
  767. if (preg_match(
  768. '/<link[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid.delegate[ \t]*[^"\']*\\1[^>]*href=(["\'])([^"\']+)\\2[^>]*\/?>/i',
  769. $response,
  770. $r)) {
  771. $realId = $r[3];
  772. } else if (preg_match(
  773. '/<link[^>]*href=(["\'])([^"\']+)\\1[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid.delegate[ \t]*[^"\']*\\3[^>]*\/?>/i',
  774. $response,
  775. $r)) {
  776. $realId = $r[2];
  777. }
  778. }
  779. $expire = time() + 60 * 60;
  780. $this->_storage->addDiscoveryInfo($id, $realId, $server, $version, $expire);
  781. $id = $realId;
  782. return true;
  783. }
  784. /**
  785. * Performs check of OpenID identity.
  786. *
  787. * This is the first step of OpenID authentication process.
  788. * On success the function does not return (it does HTTP redirection to
  789. * server and exits). On failure it returns false.
  790. *
  791. * @param bool $immediate enables or disables interaction with user
  792. * @param string $id OpenID identity
  793. * @param string $returnTo HTTP URL to redirect response from server to
  794. * @param string $root HTTP URL to identify consumer on server
  795. * @param mixed $extensions extension object or array of extensions objects
  796. * @param Zend_Controller_Response_Abstract $response an optional response
  797. * object to perform HTTP or HTML form redirection
  798. * @return bool
  799. */
  800. protected function _checkId($immediate, $id, $returnTo=null, $root=null,
  801. $extensions=null, Zend_Controller_Response_Abstract $response = null)
  802. {
  803. $this->_setError('');
  804. if (!Zend_OpenId::normalize($id)) {
  805. $this->_setError("Normalisation failed");
  806. return false;
  807. }
  808. $claimedId = $id;
  809. if (!$this->_discovery($id, $server, $version)) {
  810. $this->_setError("Discovery failed: " . $this->getError());
  811. return false;
  812. }
  813. if (!$this->_associate($server, $version)) {
  814. $this->_setError("Association failed: " . $this->getError());
  815. return false;
  816. }
  817. if (!$this->_getAssociation(
  818. $server,
  819. $handle,
  820. $macFunc,
  821. $secret,
  822. $expires)) {
  823. /* Use dumb mode */
  824. unset($handle);
  825. unset($macFunc);
  826. unset($secret);
  827. unset($expires);
  828. }
  829. $params = array();
  830. if ($version >= 2.0) {
  831. $params['openid.ns'] = Zend_OpenId::NS_2_0;
  832. }
  833. $params['openid.mode'] = $immediate ?
  834. 'checkid_immediate' : 'checkid_setup';
  835. $params['openid.identity'] = $id;
  836. $params['openid.claimed_id'] = $claimedId;
  837. if ($version <= 2.0) {
  838. if ($this->_session !== null) {
  839. $this->_session->identity = $id;
  840. $this->_session->claimed_id = $claimedId;
  841. } else if (defined('SID')) {
  842. $_SESSION["zend_openid"] = array(
  843. "identity" => $id,
  844. "claimed_id" => $claimedId);
  845. } else {
  846. require_once "Zend/Session/Namespace.php";
  847. $this->_session = new Zend_Session_Namespace("zend_openid");
  848. $this->_session->identity = $id;
  849. $this->_session->claimed_id = $claimedId;
  850. }
  851. }
  852. if (isset($handle)) {
  853. $params['openid.assoc_handle'] = $handle;
  854. }
  855. $params['openid.return_to'] = Zend_OpenId::absoluteUrl($returnTo);
  856. if (empty($root)) {
  857. $root = Zend_OpenId::selfUrl();
  858. if ($root[strlen($root)-1] != '/') {
  859. $root = dirname($root);
  860. }
  861. }
  862. if ($version >= 2.0) {
  863. $params['openid.realm'] = $root;
  864. } else {
  865. $params['openid.trust_root'] = $root;
  866. }
  867. if (!Zend_OpenId_Extension::forAll($extensions, 'prepareRequest', $params)) {
  868. $this->_setError("Extension::prepareRequest failure");
  869. return false;
  870. }
  871. Zend_OpenId::redirect($server, $params, $response);
  872. return true;
  873. }
  874. /**
  875. * Sets HTTP client object to make HTTP requests
  876. *
  877. * @param Zend_Http_Client $client HTTP client object to be used
  878. */
  879. public function setHttpClient($client) {
  880. $this->_httpClient = $client;
  881. }
  882. /**
  883. * Returns HTTP client object that will be used to make HTTP requests
  884. *
  885. * @return Zend_Http_Client
  886. */
  887. public function getHttpClient() {
  888. return $this->_httpClient;
  889. }
  890. /**
  891. * Sets session object to store climed_id
  892. *
  893. * @param Zend_Session_Namespace $session HTTP client object to be used
  894. */
  895. public function setSession(Zend_Session_Namespace $session) {
  896. $this->_session = $session;
  897. }
  898. /**
  899. * Returns session object that is used to store climed_id
  900. *
  901. * @return Zend_Session_Namespace
  902. */
  903. public function getSession() {
  904. return $this->_session;
  905. }
  906. /**
  907. * Saves error message
  908. *
  909. * @param string $message error message
  910. */
  911. protected function _setError($message)
  912. {
  913. $this->_error = $message;
  914. }
  915. /**
  916. * Returns error message that explains failure of login, check or verify
  917. *
  918. * @return string
  919. */
  920. public function getError()
  921. {
  922. return $this->_error;
  923. }
  924. }