PageRenderTime 65ms CodeModel.GetById 37ms RepoModel.GetById 0ms app.codeStats 0ms

/php-openid-1.2.3/Auth/OpenID/Server.php

https://github.com/Mercedes/openid-server
PHP | 1307 lines | 878 code | 117 blank | 312 comment | 63 complexity | f2d0544e39ec72aad5205a37aeddcc3a MD5 | raw file
Possible License(s): LGPL-2.1
  1. <?php
  2. /**
  3. * OpenID server protocol and logic.
  4. *
  5. * Overview
  6. *
  7. * An OpenID server must perform three tasks:
  8. *
  9. * 1. Examine the incoming request to determine its nature and validity.
  10. * 2. Make a decision about how to respond to this request.
  11. * 3. Format the response according to the protocol.
  12. *
  13. * The first and last of these tasks may performed by the
  14. * 'decodeRequest' and 'encodeResponse' methods of the
  15. * Auth_OpenID_Server object. Who gets to do the intermediate task --
  16. * deciding how to respond to the request -- will depend on what type
  17. * of request it is.
  18. *
  19. * If it's a request to authenticate a user (a 'checkid_setup' or
  20. * 'checkid_immediate' request), you need to decide if you will assert
  21. * that this user may claim the identity in question. Exactly how you
  22. * do that is a matter of application policy, but it generally
  23. * involves making sure the user has an account with your system and
  24. * is logged in, checking to see if that identity is hers to claim,
  25. * and verifying with the user that she does consent to releasing that
  26. * information to the party making the request.
  27. *
  28. * Examine the properties of the Auth_OpenID_CheckIDRequest object,
  29. * and if and when you've come to a decision, form a response by
  30. * calling Auth_OpenID_CheckIDRequest::answer.
  31. *
  32. * Other types of requests relate to establishing associations between
  33. * client and server and verifing the authenticity of previous
  34. * communications. Auth_OpenID_Server contains all the logic and data
  35. * necessary to respond to such requests; just pass it to
  36. * Auth_OpenID_Server::handleRequest.
  37. *
  38. * OpenID Extensions
  39. *
  40. * Do you want to provide other information for your users in addition
  41. * to authentication? Version 1.2 of the OpenID protocol allows
  42. * consumers to add extensions to their requests. For example, with
  43. * sites using the Simple Registration
  44. * Extension
  45. * (http://www.openidenabled.com/openid/simple-registration-extension/),
  46. * a user can agree to have their nickname and e-mail address sent to
  47. * a site when they sign up.
  48. *
  49. * Since extensions do not change the way OpenID authentication works,
  50. * code to handle extension requests may be completely separate from
  51. * the Auth_OpenID_Request class here. But you'll likely want data
  52. * sent back by your extension to be signed. Auth_OpenID_Response
  53. * provides methods with which you can add data to it which can be
  54. * signed with the other data in the OpenID signature.
  55. *
  56. * For example:
  57. *
  58. * // when request is a checkid_* request
  59. * response = request.answer(True)
  60. * // this will a signed 'openid.sreg.timezone' parameter to the response
  61. * response.addField('sreg', 'timezone', 'America/Los_Angeles')
  62. *
  63. * Stores
  64. *
  65. * The OpenID server needs to maintain state between requests in order
  66. * to function. Its mechanism for doing this is called a store. The
  67. * store interface is defined in Interface.php. Additionally, several
  68. * concrete store implementations are provided, so that most sites
  69. * won't need to implement a custom store. For a store backed by flat
  70. * files on disk, see Auth_OpenID_FileStore. For stores based on
  71. * MySQL, SQLite, or PostgreSQL, see the Auth_OpenID_SQLStore
  72. * subclasses.
  73. *
  74. * Upgrading
  75. *
  76. * The keys by which a server looks up associations in its store have
  77. * changed in version 1.2 of this library. If your store has entries
  78. * created from version 1.0 code, you should empty it.
  79. *
  80. * PHP versions 4 and 5
  81. *
  82. * LICENSE: See the COPYING file included in this distribution.
  83. *
  84. * @package OpenID
  85. * @author JanRain, Inc. <openid@janrain.com>
  86. * @copyright 2005 Janrain, Inc.
  87. * @license http://www.gnu.org/copyleft/lesser.html LGPL
  88. */
  89. /**
  90. * Required imports
  91. */
  92. require_once "Auth/OpenID.php";
  93. require_once "Auth/OpenID/Association.php";
  94. require_once "Auth/OpenID/CryptUtil.php";
  95. require_once "Auth/OpenID/BigMath.php";
  96. require_once "Auth/OpenID/DiffieHellman.php";
  97. require_once "Auth/OpenID/KVForm.php";
  98. require_once "Auth/OpenID/TrustRoot.php";
  99. require_once "Auth/OpenID/ServerRequest.php";
  100. define('AUTH_OPENID_HTTP_OK', 200);
  101. define('AUTH_OPENID_HTTP_REDIRECT', 302);
  102. define('AUTH_OPENID_HTTP_ERROR', 400);
  103. global $_Auth_OpenID_Request_Modes,
  104. $_Auth_OpenID_OpenID_Prefix,
  105. $_Auth_OpenID_Encode_Kvform,
  106. $_Auth_OpenID_Encode_Url;
  107. /**
  108. * @access private
  109. */
  110. $_Auth_OpenID_Request_Modes = array('checkid_setup',
  111. 'checkid_immediate');
  112. /**
  113. * @access private
  114. */
  115. $_Auth_OpenID_OpenID_Prefix = "openid.";
  116. /**
  117. * @access private
  118. */
  119. $_Auth_OpenID_Encode_Kvform = array('kfvorm');
  120. /**
  121. * @access private
  122. */
  123. $_Auth_OpenID_Encode_Url = array('URL/redirect');
  124. /**
  125. * @access private
  126. */
  127. function _isError($obj, $cls = 'Auth_OpenID_ServerError')
  128. {
  129. return is_a($obj, $cls);
  130. }
  131. /**
  132. * An error class which gets instantiated and returned whenever an
  133. * OpenID protocol error occurs. Be prepared to use this in place of
  134. * an ordinary server response.
  135. *
  136. * @package OpenID
  137. */
  138. class Auth_OpenID_ServerError {
  139. /**
  140. * @access private
  141. */
  142. function Auth_OpenID_ServerError($query = null, $message = null)
  143. {
  144. $this->message = $message;
  145. $this->query = $query;
  146. }
  147. /**
  148. * Returns the return_to URL for the request which caused this
  149. * error.
  150. */
  151. function hasReturnTo()
  152. {
  153. global $_Auth_OpenID_OpenID_Prefix;
  154. if ($this->query) {
  155. return array_key_exists($_Auth_OpenID_OpenID_Prefix .
  156. 'return_to', $this->query);
  157. } else {
  158. return false;
  159. }
  160. }
  161. /**
  162. * Encodes this error's response as a URL suitable for
  163. * redirection. If the response has no return_to, another
  164. * Auth_OpenID_ServerError is returned.
  165. */
  166. function encodeToURL()
  167. {
  168. global $_Auth_OpenID_OpenID_Prefix;
  169. $return_to = Auth_OpenID::arrayGet($this->query,
  170. $_Auth_OpenID_OpenID_Prefix .
  171. 'return_to');
  172. if (!$return_to) {
  173. return new Auth_OpenID_ServerError(null, "no return_to URL");
  174. }
  175. return Auth_OpenID::appendArgs($return_to,
  176. array('openid.mode' => 'error',
  177. 'openid.error' => $this->toString()));
  178. }
  179. /**
  180. * Encodes the response to key-value form. This is a
  181. * machine-readable format used to respond to messages which came
  182. * directly from the consumer and not through the user-agent. See
  183. * the OpenID specification.
  184. */
  185. function encodeToKVForm()
  186. {
  187. return Auth_OpenID_KVForm::fromArray(
  188. array('mode' => 'error',
  189. 'error' => $this->toString()));
  190. }
  191. /**
  192. * Returns one of $_Auth_OpenID_Encode_Url,
  193. * $_Auth_OpenID_Encode_Kvform, or null, depending on the type of
  194. * encoding expected for this error's payload.
  195. */
  196. function whichEncoding()
  197. {
  198. global $_Auth_OpenID_Encode_Url,
  199. $_Auth_OpenID_Encode_Kvform,
  200. $_Auth_OpenID_Request_Modes;
  201. if ($this->hasReturnTo()) {
  202. return $_Auth_OpenID_Encode_Url;
  203. }
  204. $mode = Auth_OpenID::arrayGet($this->query, 'openid.mode');
  205. if ($mode) {
  206. if (!in_array($mode, $_Auth_OpenID_Request_Modes)) {
  207. return $_Auth_OpenID_Encode_Kvform;
  208. }
  209. }
  210. return null;
  211. }
  212. /**
  213. * Returns this error message.
  214. */
  215. function toString()
  216. {
  217. if ($this->message) {
  218. return $this->message;
  219. } else {
  220. return get_class($this) . " error";
  221. }
  222. }
  223. }
  224. /**
  225. * An error indicating that the return_to URL is malformed.
  226. *
  227. * @package OpenID
  228. */
  229. class Auth_OpenID_MalformedReturnURL extends Auth_OpenID_ServerError {
  230. function Auth_OpenID_MalformedReturnURL($query, $return_to)
  231. {
  232. $this->return_to = $return_to;
  233. parent::Auth_OpenID_ServerError($query, "malformed return_to URL");
  234. }
  235. }
  236. /**
  237. * This error is returned when the trust_root value is malformed.
  238. *
  239. * @package OpenID
  240. */
  241. class Auth_OpenID_MalformedTrustRoot extends Auth_OpenID_ServerError {
  242. function toString()
  243. {
  244. return "Malformed trust root";
  245. }
  246. }
  247. /**
  248. * The base class for all server request classes.
  249. *
  250. * @access private
  251. * @package OpenID
  252. */
  253. class Auth_OpenID_Request {
  254. var $mode = null;
  255. }
  256. /**
  257. * A request to verify the validity of a previous response.
  258. *
  259. * @access private
  260. * @package OpenID
  261. */
  262. class Auth_OpenID_CheckAuthRequest extends Auth_OpenID_Request {
  263. var $mode = "check_authentication";
  264. var $invalidate_handle = null;
  265. function Auth_OpenID_CheckAuthRequest($assoc_handle, $sig, $signed,
  266. $invalidate_handle = null)
  267. {
  268. $this->assoc_handle = $assoc_handle;
  269. $this->sig = $sig;
  270. $this->signed = $signed;
  271. if ($invalidate_handle !== null) {
  272. $this->invalidate_handle = $invalidate_handle;
  273. }
  274. }
  275. function fromQuery($query)
  276. {
  277. global $_Auth_OpenID_OpenID_Prefix;
  278. $required_keys = array('assoc_handle', 'sig', 'signed');
  279. foreach ($required_keys as $k) {
  280. if (!array_key_exists($_Auth_OpenID_OpenID_Prefix . $k,
  281. $query)) {
  282. return new Auth_OpenID_ServerError($query,
  283. sprintf("%s request missing required parameter %s from \
  284. query", "check_authentication", $k));
  285. }
  286. }
  287. $assoc_handle = $query[$_Auth_OpenID_OpenID_Prefix . 'assoc_handle'];
  288. $sig = $query[$_Auth_OpenID_OpenID_Prefix . 'sig'];
  289. $signed_list = $query[$_Auth_OpenID_OpenID_Prefix . 'signed'];
  290. $signed_list = explode(",", $signed_list);
  291. $signed_pairs = array();
  292. foreach ($signed_list as $field) {
  293. if ($field == 'mode') {
  294. // XXX KLUDGE HAX WEB PROTOCoL BR0KENNN
  295. //
  296. // openid.mode is currently check_authentication
  297. // because that's the mode of this request. But the
  298. // signature was made on something with a different
  299. // openid.mode.
  300. $value = "id_res";
  301. } else {
  302. if (array_key_exists($_Auth_OpenID_OpenID_Prefix . $field,
  303. $query)) {
  304. $value = $query[$_Auth_OpenID_OpenID_Prefix . $field];
  305. } else {
  306. return new Auth_OpenID_ServerError($query,
  307. sprintf("Couldn't find signed field %r in query %s",
  308. $field, var_export($query, true)));
  309. }
  310. }
  311. $signed_pairs[] = array($field, $value);
  312. }
  313. $result = new Auth_OpenID_CheckAuthRequest($assoc_handle, $sig,
  314. $signed_pairs);
  315. $result->invalidate_handle = Auth_OpenID::arrayGet($query,
  316. $_Auth_OpenID_OpenID_Prefix . 'invalidate_handle');
  317. return $result;
  318. }
  319. function answer(&$signatory)
  320. {
  321. $is_valid = $signatory->verify($this->assoc_handle, $this->sig,
  322. $this->signed);
  323. // Now invalidate that assoc_handle so it this checkAuth
  324. // message cannot be replayed.
  325. $signatory->invalidate($this->assoc_handle, true);
  326. $response = new Auth_OpenID_ServerResponse($this);
  327. $response->fields['is_valid'] = $is_valid ? "true" : "false";
  328. if ($this->invalidate_handle) {
  329. $assoc = $signatory->getAssociation($this->invalidate_handle,
  330. false);
  331. if (!$assoc) {
  332. $response->fields['invalidate_handle'] =
  333. $this->invalidate_handle;
  334. }
  335. }
  336. return $response;
  337. }
  338. }
  339. class Auth_OpenID_PlainTextServerSession {
  340. /**
  341. * An object that knows how to handle association requests with no
  342. * session type.
  343. */
  344. var $session_type = 'plaintext';
  345. function fromQuery($unused_request)
  346. {
  347. return new Auth_OpenID_PlainTextServerSession();
  348. }
  349. function answer($secret)
  350. {
  351. return array('mac_key' => base64_encode($secret));
  352. }
  353. }
  354. class Auth_OpenID_DiffieHellmanServerSession {
  355. /**
  356. * An object that knows how to handle association requests with
  357. * the Diffie-Hellman session type.
  358. */
  359. var $session_type = 'DH-SHA1';
  360. function Auth_OpenID_DiffieHellmanServerSession($dh, $consumer_pubkey)
  361. {
  362. $this->dh = $dh;
  363. $this->consumer_pubkey = $consumer_pubkey;
  364. }
  365. function fromQuery($query)
  366. {
  367. $dh_modulus = Auth_OpenID::arrayGet($query, 'openid.dh_modulus');
  368. $dh_gen = Auth_OpenID::arrayGet($query, 'openid.dh_gen');
  369. if ((($dh_modulus === null) && ($dh_gen !== null)) ||
  370. (($dh_gen === null) && ($dh_modulus !== null))) {
  371. if ($dh_modulus === null) {
  372. $missing = 'modulus';
  373. } else {
  374. $missing = 'generator';
  375. }
  376. return new Auth_OpenID_ServerError(
  377. 'If non-default modulus or generator is '.
  378. 'supplied, both must be supplied. Missing '.
  379. $missing);
  380. }
  381. $lib =& Auth_OpenID_getMathLib();
  382. if ($dh_modulus || $dh_gen) {
  383. $dh_modulus = $lib->base64ToLong($dh_modulus);
  384. $dh_gen = $lib->base64ToLong($dh_gen);
  385. if ($lib->cmp($dh_modulus, 0) == 0 ||
  386. $lib->cmp($dh_gen, 0) == 0) {
  387. return new Auth_OpenID_ServerError(
  388. $query, "Failed to parse dh_mod or dh_gen");
  389. }
  390. $dh = new Auth_OpenID_DiffieHellman($dh_modulus, $dh_gen);
  391. } else {
  392. $dh = new Auth_OpenID_DiffieHellman();
  393. }
  394. $consumer_pubkey = Auth_OpenID::arrayGet($query,
  395. 'openid.dh_consumer_public');
  396. if ($consumer_pubkey === null) {
  397. return new Auth_OpenID_ServerError(
  398. 'Public key for DH-SHA1 session '.
  399. 'not found in query');
  400. }
  401. $consumer_pubkey =
  402. $lib->base64ToLong($consumer_pubkey);
  403. if ($consumer_pubkey === false) {
  404. return new Auth_OpenID_ServerError($query,
  405. "dh_consumer_public is not base64");
  406. }
  407. return new Auth_OpenID_DiffieHellmanServerSession($dh,
  408. $consumer_pubkey);
  409. }
  410. function answer($secret)
  411. {
  412. $lib =& Auth_OpenID_getMathLib();
  413. $mac_key = $this->dh->xorSecret($this->consumer_pubkey, $secret);
  414. return array(
  415. 'dh_server_public' =>
  416. $lib->longToBase64($this->dh->public),
  417. 'enc_mac_key' => base64_encode($mac_key));
  418. }
  419. }
  420. /**
  421. * A request to associate with the server.
  422. *
  423. * @access private
  424. * @package OpenID
  425. */
  426. class Auth_OpenID_AssociateRequest extends Auth_OpenID_Request {
  427. var $mode = "associate";
  428. var $assoc_type = 'HMAC-SHA1';
  429. function Auth_OpenID_AssociateRequest(&$session)
  430. {
  431. $this->session =& $session;
  432. }
  433. function fromQuery($query)
  434. {
  435. global $_Auth_OpenID_OpenID_Prefix;
  436. $session_classes = array(
  437. 'DH-SHA1' => 'Auth_OpenID_DiffieHellmanServerSession',
  438. null => 'Auth_OpenID_PlainTextServerSession');
  439. $session_type = null;
  440. if (array_key_exists($_Auth_OpenID_OpenID_Prefix . 'session_type',
  441. $query)) {
  442. $session_type = $query[$_Auth_OpenID_OpenID_Prefix .
  443. 'session_type'];
  444. }
  445. if (!array_key_exists($session_type, $session_classes)) {
  446. return new Auth_OpenID_ServerError($query,
  447. "Unknown session type $session_type");
  448. }
  449. $session_cls = $session_classes[$session_type];
  450. $session = call_user_func_array(array($session_cls, 'fromQuery'),
  451. array($query));
  452. if (($session === null) || (_isError($session))) {
  453. return new Auth_OpenID_ServerError($query,
  454. "Error parsing $session_type session");
  455. }
  456. return new Auth_OpenID_AssociateRequest($session);
  457. }
  458. function answer($assoc)
  459. {
  460. $ml =& Auth_OpenID_getMathLib();
  461. $response = new Auth_OpenID_ServerResponse($this);
  462. $response->fields = array('expires_in' => $assoc->getExpiresIn(),
  463. 'assoc_type' => 'HMAC-SHA1',
  464. 'assoc_handle' => $assoc->handle);
  465. $r = $this->session->answer($assoc->secret);
  466. foreach ($r as $k => $v) {
  467. $response->fields[$k] = $v;
  468. }
  469. if ($this->session->session_type != 'plaintext') {
  470. $response->fields['session_type'] = $this->session->session_type;
  471. }
  472. return $response;
  473. }
  474. }
  475. /**
  476. * A request to confirm the identity of a user.
  477. *
  478. * @access private
  479. * @package OpenID
  480. */
  481. class Auth_OpenID_CheckIDRequest extends Auth_OpenID_Request {
  482. var $mode = "checkid_setup"; // or "checkid_immediate"
  483. var $immediate = false;
  484. var $trust_root = null;
  485. function make($query, $identity, $return_to, $trust_root = null,
  486. $immediate = false, $assoc_handle = null)
  487. {
  488. if (!Auth_OpenID_TrustRoot::_parse($return_to)) {
  489. return new Auth_OpenID_MalformedReturnURL($query, $return_to);
  490. }
  491. $r = new Auth_OpenID_CheckIDRequest($identity, $return_to,
  492. $trust_root, $immediate,
  493. $assoc_handle);
  494. if (!$r->trustRootValid()) {
  495. return new Auth_OpenID_UntrustedReturnURL($return_to,
  496. $trust_root);
  497. } else {
  498. return $r;
  499. }
  500. }
  501. function Auth_OpenID_CheckIDRequest($identity, $return_to,
  502. $trust_root = null, $immediate = false,
  503. $assoc_handle = null)
  504. {
  505. $this->identity = $identity;
  506. $this->return_to = $return_to;
  507. $this->trust_root = $trust_root;
  508. $this->assoc_handle = $assoc_handle;
  509. if ($immediate) {
  510. $this->immediate = true;
  511. $this->mode = "checkid_immediate";
  512. } else {
  513. $this->immediate = false;
  514. $this->mode = "checkid_setup";
  515. }
  516. }
  517. function fromQuery($query)
  518. {
  519. global $_Auth_OpenID_OpenID_Prefix;
  520. $mode = $query[$_Auth_OpenID_OpenID_Prefix . 'mode'];
  521. $immediate = null;
  522. if ($mode == "checkid_immediate") {
  523. $immediate = true;
  524. $mode = "checkid_immediate";
  525. } else {
  526. $immediate = false;
  527. $mode = "checkid_setup";
  528. }
  529. $required = array('identity',
  530. 'return_to');
  531. $optional = array('trust_root',
  532. 'assoc_handle');
  533. $values = array();
  534. foreach ($required as $field) {
  535. if (array_key_exists($_Auth_OpenID_OpenID_Prefix . $field,
  536. $query)) {
  537. $value = $query[$_Auth_OpenID_OpenID_Prefix . $field];
  538. } else {
  539. return new Auth_OpenID_ServerError($query,
  540. sprintf("Missing required field %s from request",
  541. $field));
  542. }
  543. $values[$field] = $value;
  544. }
  545. foreach ($optional as $field) {
  546. $value = null;
  547. if (array_key_exists($_Auth_OpenID_OpenID_Prefix . $field,
  548. $query)) {
  549. $value = $query[$_Auth_OpenID_OpenID_Prefix. $field];
  550. }
  551. if ($value) {
  552. $values[$field] = $value;
  553. }
  554. }
  555. if (!Auth_OpenID_TrustRoot::_parse($values['return_to'])) {
  556. return new Auth_OpenID_MalformedReturnURL($query,
  557. $values['return_to']);
  558. }
  559. $obj = Auth_OpenID_CheckIDRequest::make($query,
  560. $values['identity'],
  561. $values['return_to'],
  562. Auth_OpenID::arrayGet($values,
  563. 'trust_root', null),
  564. $immediate);
  565. if (is_a($obj, 'Auth_OpenID_ServerError')) {
  566. return $obj;
  567. }
  568. if (Auth_OpenID::arrayGet($values, 'assoc_handle')) {
  569. $obj->assoc_handle = $values['assoc_handle'];
  570. }
  571. return $obj;
  572. }
  573. function trustRootValid()
  574. {
  575. if (!$this->trust_root) {
  576. return true;
  577. }
  578. $tr = Auth_OpenID_TrustRoot::_parse($this->trust_root);
  579. if ($tr === false) {
  580. return new Auth_OpenID_MalformedTrustRoot(null, $this->trust_root);
  581. }
  582. return Auth_OpenID_TrustRoot::match($this->trust_root,
  583. $this->return_to);
  584. }
  585. function answer($allow, $server_url = null)
  586. {
  587. if ($allow || $this->immediate) {
  588. $mode = 'id_res';
  589. } else {
  590. $mode = 'cancel';
  591. }
  592. $response = new Auth_OpenID_CheckIDResponse($this, $mode);
  593. if ($allow) {
  594. $response->fields['identity'] = $this->identity;
  595. $response->fields['return_to'] = $this->return_to;
  596. if (!$this->trustRootValid()) {
  597. return new Auth_OpenID_UntrustedReturnURL($this->return_to,
  598. $this->trust_root);
  599. }
  600. } else {
  601. $response->signed = array();
  602. if ($this->immediate) {
  603. if (!$server_url) {
  604. return new Auth_OpenID_ServerError(null,
  605. 'setup_url is required for $allow=false \
  606. in immediate mode.');
  607. }
  608. $setup_request =& new Auth_OpenID_CheckIDRequest(
  609. $this->identity,
  610. $this->return_to,
  611. $this->trust_root,
  612. false,
  613. $this->assoc_handle);
  614. $setup_url = $setup_request->encodeToURL($server_url);
  615. $response->fields['user_setup_url'] = $setup_url;
  616. }
  617. }
  618. return $response;
  619. }
  620. function encodeToURL($server_url)
  621. {
  622. global $_Auth_OpenID_OpenID_Prefix;
  623. // Imported from the alternate reality where these classes are
  624. // used in both the client and server code, so Requests are
  625. // Encodable too. That's right, code imported from alternate
  626. // realities all for the love of you, id_res/user_setup_url.
  627. $q = array('mode' => $this->mode,
  628. 'identity' => $this->identity,
  629. 'return_to' => $this->return_to);
  630. if ($this->trust_root) {
  631. $q['trust_root'] = $this->trust_root;
  632. }
  633. if ($this->assoc_handle) {
  634. $q['assoc_handle'] = $this->assoc_handle;
  635. }
  636. $_q = array();
  637. foreach ($q as $k => $v) {
  638. $_q[$_Auth_OpenID_OpenID_Prefix . $k] = $v;
  639. }
  640. return Auth_OpenID::appendArgs($server_url, $_q);
  641. }
  642. function getCancelURL()
  643. {
  644. global $_Auth_OpenID_OpenID_Prefix;
  645. if ($this->immediate) {
  646. return new Auth_OpenID_ServerError(null,
  647. "Cancel is not an appropriate \
  648. response to immediate mode \
  649. requests.");
  650. }
  651. return Auth_OpenID::appendArgs($this->return_to,
  652. array($_Auth_OpenID_OpenID_Prefix . 'mode' =>
  653. 'cancel'));
  654. }
  655. }
  656. /**
  657. * This class encapsulates the response to an OpenID server request.
  658. *
  659. * @access private
  660. * @package OpenID
  661. */
  662. class Auth_OpenID_ServerResponse {
  663. function Auth_OpenID_ServerResponse($request)
  664. {
  665. $this->request = $request;
  666. $this->fields = array();
  667. }
  668. function whichEncoding()
  669. {
  670. global $_Auth_OpenID_Encode_Kvform,
  671. $_Auth_OpenID_Request_Modes,
  672. $_Auth_OpenID_Encode_Url;
  673. if (in_array($this->request->mode, $_Auth_OpenID_Request_Modes)) {
  674. return $_Auth_OpenID_Encode_Url;
  675. } else {
  676. return $_Auth_OpenID_Encode_Kvform;
  677. }
  678. }
  679. function encodeToURL()
  680. {
  681. global $_Auth_OpenID_OpenID_Prefix;
  682. $fields = array();
  683. foreach ($this->fields as $k => $v) {
  684. $fields[$_Auth_OpenID_OpenID_Prefix . $k] = $v;
  685. }
  686. return Auth_OpenID::appendArgs($this->request->return_to, $fields);
  687. }
  688. function encodeToKVForm()
  689. {
  690. return Auth_OpenID_KVForm::fromArray($this->fields);
  691. }
  692. }
  693. /**
  694. * A response to a checkid request.
  695. *
  696. * @access private
  697. * @package OpenID
  698. */
  699. class Auth_OpenID_CheckIDResponse extends Auth_OpenID_ServerResponse {
  700. function Auth_OpenID_CheckIDResponse(&$request, $mode = 'id_res')
  701. {
  702. parent::Auth_OpenID_ServerResponse($request);
  703. $this->fields['mode'] = $mode;
  704. $this->signed = array();
  705. if ($mode == 'id_res') {
  706. array_push($this->signed, 'mode', 'identity', 'return_to');
  707. }
  708. }
  709. function addField($namespace, $key, $value, $signed = true)
  710. {
  711. if ($namespace) {
  712. $key = sprintf('%s.%s', $namespace, $key);
  713. }
  714. $this->fields[$key] = $value;
  715. if ($signed && !in_array($key, $this->signed)) {
  716. $this->signed[] = $key;
  717. }
  718. }
  719. function addFields($namespace, $fields, $signed = true)
  720. {
  721. foreach ($fields as $k => $v) {
  722. $this->addField($namespace, $k, $v, $signed);
  723. }
  724. }
  725. function update($namespace, $other)
  726. {
  727. $namespaced_fields = array();
  728. foreach ($other->fields as $k => $v) {
  729. $name = sprintf('%s.%s', $namespace, $k);
  730. $namespaced_fields[$name] = $v;
  731. }
  732. $this->fields = array_merge($this->fields, $namespaced_fields);
  733. $this->signed = array_merge($this->signed, $other->signed);
  734. }
  735. }
  736. /**
  737. * A web-capable response object which you can use to generate a
  738. * user-agent response.
  739. *
  740. * @package OpenID
  741. */
  742. class Auth_OpenID_WebResponse {
  743. var $code = AUTH_OPENID_HTTP_OK;
  744. var $body = "";
  745. function Auth_OpenID_WebResponse($code = null, $headers = null,
  746. $body = null)
  747. {
  748. if ($code) {
  749. $this->code = $code;
  750. }
  751. if ($headers !== null) {
  752. $this->headers = $headers;
  753. } else {
  754. $this->headers = array();
  755. }
  756. if ($body !== null) {
  757. $this->body = $body;
  758. }
  759. }
  760. }
  761. /**
  762. * Responsible for the signature of query data and the verification of
  763. * OpenID signature values.
  764. *
  765. * @package OpenID
  766. */
  767. class Auth_OpenID_Signatory {
  768. // = 14 * 24 * 60 * 60; # 14 days, in seconds
  769. var $SECRET_LIFETIME = 1209600;
  770. // keys have a bogus server URL in them because the filestore
  771. // really does expect that key to be a URL. This seems a little
  772. // silly for the server store, since I expect there to be only one
  773. // server URL.
  774. var $normal_key = 'http://localhost/|normal';
  775. var $dumb_key = 'http://localhost/|dumb';
  776. /**
  777. * Create a new signatory using a given store.
  778. */
  779. function Auth_OpenID_Signatory(&$store)
  780. {
  781. // assert store is not None
  782. $this->store =& $store;
  783. }
  784. /**
  785. * Verify, using a given association handle, a signature with
  786. * signed key-value pairs from an HTTP request.
  787. */
  788. function verify($assoc_handle, $sig, $signed_pairs)
  789. {
  790. $assoc = $this->getAssociation($assoc_handle, true);
  791. if (!$assoc) {
  792. // oidutil.log("failed to get assoc with handle %r to verify sig %r"
  793. // % (assoc_handle, sig))
  794. return false;
  795. }
  796. $expected_sig = base64_encode($assoc->sign($signed_pairs));
  797. return $sig == $expected_sig;
  798. }
  799. /**
  800. * Given a response, sign the fields in the response's 'signed'
  801. * list, and insert the signature into the response.
  802. */
  803. function sign($response)
  804. {
  805. $signed_response = $response;
  806. $assoc_handle = $response->request->assoc_handle;
  807. if ($assoc_handle) {
  808. // normal mode
  809. $assoc = $this->getAssociation($assoc_handle, false);
  810. if (!$assoc) {
  811. // fall back to dumb mode
  812. $signed_response->fields['invalidate_handle'] = $assoc_handle;
  813. $assoc = $this->createAssociation(true);
  814. }
  815. } else {
  816. // dumb mode.
  817. $assoc = $this->createAssociation(true);
  818. }
  819. $signed_response->fields['assoc_handle'] = $assoc->handle;
  820. $assoc->addSignature($signed_response->signed,
  821. $signed_response->fields, '');
  822. return $signed_response;
  823. }
  824. /**
  825. * Make a new association.
  826. */
  827. function createAssociation($dumb = true, $assoc_type = 'HMAC-SHA1')
  828. {
  829. $secret = Auth_OpenID_CryptUtil::getBytes(20);
  830. $uniq = base64_encode(Auth_OpenID_CryptUtil::getBytes(4));
  831. $handle = sprintf('{%s}{%x}{%s}', $assoc_type, intval(time()), $uniq);
  832. $assoc = Auth_OpenID_Association::fromExpiresIn(
  833. $this->SECRET_LIFETIME, $handle, $secret, $assoc_type);
  834. if ($dumb) {
  835. $key = $this->dumb_key;
  836. } else {
  837. $key = $this->normal_key;
  838. }
  839. $this->store->storeAssociation($key, $assoc);
  840. return $assoc;
  841. }
  842. /**
  843. * Given an association handle, get the association from the
  844. * store, or return a ServerError or null if something goes wrong.
  845. */
  846. function getAssociation($assoc_handle, $dumb)
  847. {
  848. if ($assoc_handle === null) {
  849. return new Auth_OpenID_ServerError(null,
  850. "assoc_handle must not be null");
  851. }
  852. if ($dumb) {
  853. $key = $this->dumb_key;
  854. } else {
  855. $key = $this->normal_key;
  856. }
  857. $assoc = $this->store->getAssociation($key, $assoc_handle);
  858. if (($assoc !== null) && ($assoc->getExpiresIn() <= 0)) {
  859. $this->store->removeAssociation($key, $assoc_handle);
  860. $assoc = null;
  861. }
  862. return $assoc;
  863. }
  864. /**
  865. * Invalidate a given association handle.
  866. */
  867. function invalidate($assoc_handle, $dumb)
  868. {
  869. if ($dumb) {
  870. $key = $this->dumb_key;
  871. } else {
  872. $key = $this->normal_key;
  873. }
  874. $this->store->removeAssociation($key, $assoc_handle);
  875. }
  876. }
  877. /**
  878. * Encode an Auth_OpenID_Response to an Auth_OpenID_WebResponse.
  879. *
  880. * @package OpenID
  881. */
  882. class Auth_OpenID_Encoder {
  883. var $responseFactory = 'Auth_OpenID_WebResponse';
  884. /**
  885. * Encode an Auth_OpenID_Response and return an
  886. * Auth_OpenID_WebResponse.
  887. */
  888. function encode(&$response)
  889. {
  890. global $_Auth_OpenID_Encode_Kvform,
  891. $_Auth_OpenID_Encode_Url;
  892. $cls = $this->responseFactory;
  893. $encode_as = $response->whichEncoding();
  894. if ($encode_as == $_Auth_OpenID_Encode_Kvform) {
  895. $wr = new $cls(null, null, $response->encodeToKVForm());
  896. if (is_a($response, 'Auth_OpenID_ServerError')) {
  897. $wr->code = AUTH_OPENID_HTTP_ERROR;
  898. }
  899. } else if ($encode_as == $_Auth_OpenID_Encode_Url) {
  900. $location = $response->encodeToURL();
  901. $wr = new $cls(AUTH_OPENID_HTTP_REDIRECT,
  902. array('location' => $location));
  903. } else {
  904. return new Auth_OpenID_EncodingError($response);
  905. }
  906. return $wr;
  907. }
  908. }
  909. /**
  910. * Returns true if the given response needs a signature.
  911. *
  912. * @access private
  913. */
  914. function needsSigning($response)
  915. {
  916. return (in_array($response->request->mode, array('checkid_setup',
  917. 'checkid_immediate')) &&
  918. $response->signed);
  919. }
  920. /**
  921. * An encoder which also takes care of signing fields when required.
  922. *
  923. * @package OpenID
  924. */
  925. class Auth_OpenID_SigningEncoder extends Auth_OpenID_Encoder {
  926. function Auth_OpenID_SigningEncoder(&$signatory)
  927. {
  928. $this->signatory =& $signatory;
  929. }
  930. /**
  931. * Sign an Auth_OpenID_Response and return an
  932. * Auth_OpenID_WebResponse.
  933. */
  934. function encode(&$response)
  935. {
  936. // the isinstance is a bit of a kludge... it means there isn't
  937. // really an adapter to make the interfaces quite match.
  938. if (!is_a($response, 'Auth_OpenID_ServerError') &&
  939. needsSigning($response)) {
  940. if (!$this->signatory) {
  941. return new Auth_OpenID_ServerError(null,
  942. "Must have a store to sign request");
  943. }
  944. if (array_key_exists('sig', $response->fields)) {
  945. return new Auth_OpenID_AlreadySigned($response);
  946. }
  947. $response = $this->signatory->sign($response);
  948. }
  949. return parent::encode($response);
  950. }
  951. }
  952. /**
  953. * Decode an incoming Auth_OpenID_WebResponse into an
  954. * Auth_OpenID_Request.
  955. *
  956. * @package OpenID
  957. */
  958. class Auth_OpenID_Decoder {
  959. function Auth_OpenID_Decoder()
  960. {
  961. global $_Auth_OpenID_OpenID_Prefix;
  962. $this->prefix = $_Auth_OpenID_OpenID_Prefix;
  963. $this->handlers = array(
  964. 'checkid_setup' => 'Auth_OpenID_CheckIDRequest',
  965. 'checkid_immediate' => 'Auth_OpenID_CheckIDRequest',
  966. 'check_authentication' => 'Auth_OpenID_CheckAuthRequest',
  967. 'associate' => 'Auth_OpenID_AssociateRequest'
  968. );
  969. }
  970. /**
  971. * Given an HTTP query in an array (key-value pairs), decode it
  972. * into an Auth_OpenID_Request object.
  973. */
  974. function decode($query)
  975. {
  976. if (!$query) {
  977. return null;
  978. }
  979. $myquery = array();
  980. foreach ($query as $k => $v) {
  981. if (strpos($k, $this->prefix) === 0) {
  982. $myquery[$k] = $v;
  983. }
  984. }
  985. if (!$myquery) {
  986. return null;
  987. }
  988. $mode = Auth_OpenID::arrayGet($myquery, $this->prefix . 'mode');
  989. if (!$mode) {
  990. return new Auth_OpenID_ServerError($query,
  991. sprintf("No %s mode found in query", $this->prefix));
  992. }
  993. $handlerCls = Auth_OpenID::arrayGet($this->handlers, $mode,
  994. $this->defaultDecoder($query));
  995. if (!is_a($handlerCls, 'Auth_OpenID_ServerError')) {
  996. return call_user_func_array(array($handlerCls, 'fromQuery'),
  997. array($query));
  998. } else {
  999. return $handlerCls;
  1000. }
  1001. }
  1002. function defaultDecoder($query)
  1003. {
  1004. $mode = $query[$this->prefix . 'mode'];
  1005. return new Auth_OpenID_ServerError($query,
  1006. sprintf("No decoder for mode %s", $mode));
  1007. }
  1008. }
  1009. /**
  1010. * An error that indicates an encoding problem occurred.
  1011. *
  1012. * @package OpenID
  1013. */
  1014. class Auth_OpenID_EncodingError {
  1015. function Auth_OpenID_EncodingError(&$response)
  1016. {
  1017. $this->response =& $response;
  1018. }
  1019. }
  1020. /**
  1021. * An error that indicates that a response was already signed.
  1022. *
  1023. * @package OpenID
  1024. */
  1025. class Auth_OpenID_AlreadySigned extends Auth_OpenID_EncodingError {
  1026. // This response is already signed.
  1027. }
  1028. /**
  1029. * An error that indicates that the given return_to is not under the
  1030. * given trust_root.
  1031. *
  1032. * @package OpenID
  1033. */
  1034. class Auth_OpenID_UntrustedReturnURL extends Auth_OpenID_ServerError {
  1035. function Auth_OpenID_UntrustedReturnURL($return_to, $trust_root)
  1036. {
  1037. global $_Auth_OpenID_OpenID_Prefix;
  1038. $query = array(
  1039. $_Auth_OpenID_OpenID_Prefix . 'return_to' => $return_to,
  1040. $_Auth_OpenID_OpenID_Prefix . 'trust_root' => $trust_root);
  1041. parent::Auth_OpenID_ServerError($query);
  1042. }
  1043. function toString()
  1044. {
  1045. global $_Auth_OpenID_OpenID_Prefix;
  1046. $return_to = $this->query[$_Auth_OpenID_OpenID_Prefix . 'return_to'];
  1047. $trust_root = $this->query[$_Auth_OpenID_OpenID_Prefix . 'trust_root'];
  1048. return sprintf("return_to %s not under trust_root %s",
  1049. $return_to, $trust_root);
  1050. }
  1051. }
  1052. /**
  1053. * An object that implements the OpenID protocol for a single URL.
  1054. *
  1055. * Use this object by calling getOpenIDResponse when you get any
  1056. * request for the server URL.
  1057. *
  1058. * @package OpenID
  1059. */
  1060. class Auth_OpenID_Server {
  1061. function Auth_OpenID_Server(&$store)
  1062. {
  1063. $this->store =& $store;
  1064. $this->signatory =& new Auth_OpenID_Signatory($this->store);
  1065. $this->encoder =& new Auth_OpenID_SigningEncoder($this->signatory);
  1066. $this->decoder =& new Auth_OpenID_Decoder();
  1067. }
  1068. /**
  1069. * Handle a request. Given an Auth_OpenID_Request object, call
  1070. * the appropriate Auth_OpenID_Server method to process the
  1071. * request and generate a response.
  1072. *
  1073. * @param Auth_OpenID_Request $request An Auth_OpenID_Request
  1074. * returned by Auth_OpenID_Server::decodeRequest.
  1075. *
  1076. * @return Auth_OpenID_Response $response A response object
  1077. * capable of generating a user-agent reply.
  1078. */
  1079. function handleRequest($request)
  1080. {
  1081. if (method_exists($this, "openid_" . $request->mode)) {
  1082. $handler = array($this, "openid_" . $request->mode);
  1083. return call_user_func($handler, $request);
  1084. }
  1085. return null;
  1086. }
  1087. /**
  1088. * The callback for 'check_authentication' messages.
  1089. *
  1090. * @access private
  1091. */
  1092. function openid_check_authentication(&$request)
  1093. {
  1094. return $request->answer($this->signatory);
  1095. }
  1096. /**
  1097. * The callback for 'associate' messages.
  1098. *
  1099. * @access private
  1100. */
  1101. function openid_associate(&$request)
  1102. {
  1103. $assoc = $this->signatory->createAssociation(false);
  1104. return $request->answer($assoc);
  1105. }
  1106. /**
  1107. * Encodes as response in the appropriate format suitable for
  1108. * sending to the user agent.
  1109. */
  1110. function encodeResponse(&$response)
  1111. {
  1112. return $this->encoder->encode($response);
  1113. }
  1114. /**
  1115. * Decodes a query args array into the appropriate
  1116. * Auth_OpenID_Request object.
  1117. */
  1118. function decodeRequest(&$query)
  1119. {
  1120. return $this->decoder->decode($query);
  1121. }
  1122. }
  1123. ?>