PageRenderTime 46ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/Auth/OpenID/Association.php

http://github.com/openid/php-openid
PHP | 631 lines | 312 code | 78 blank | 241 comment | 57 complexity | 9982e5a963765741b85568a43cc8adeb MD5 | raw file
Possible License(s): Apache-2.0
  1. <?php
  2. /**
  3. * This module contains code for dealing with associations between
  4. * consumers and servers.
  5. *
  6. * PHP versions 4 and 5
  7. *
  8. * LICENSE: See the COPYING file included in this distribution.
  9. *
  10. * @package OpenID
  11. * @author JanRain, Inc. <openid@janrain.com>
  12. * @copyright 2005-2008 Janrain, Inc.
  13. * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
  14. */
  15. /**
  16. * @access private
  17. */
  18. require_once 'Auth/OpenID/CryptUtil.php';
  19. /**
  20. * @access private
  21. */
  22. require_once 'Auth/OpenID/KVForm.php';
  23. /**
  24. * @access private
  25. */
  26. require_once 'Auth/OpenID/HMAC.php';
  27. /**
  28. * This class represents an association between a server and a
  29. * consumer. In general, users of this library will never see
  30. * instances of this object. The only exception is if you implement a
  31. * custom {@link Auth_OpenID_OpenIDStore}.
  32. *
  33. * If you do implement such a store, it will need to store the values
  34. * of the handle, secret, issued, lifetime, and assoc_type instance
  35. * variables.
  36. *
  37. * @package OpenID
  38. */
  39. class Auth_OpenID_Association {
  40. /**
  41. * This is a HMAC-SHA1 specific value.
  42. *
  43. * @access private
  44. */
  45. public $SIG_LENGTH = 20;
  46. /**
  47. * The ordering and name of keys as stored by serialize.
  48. *
  49. * @access private
  50. */
  51. public $assoc_keys = [
  52. 'version',
  53. 'handle',
  54. 'secret',
  55. 'issued',
  56. 'lifetime',
  57. 'assoc_type',
  58. ];
  59. public $_macs = [
  60. 'HMAC-SHA1' => 'Auth_OpenID_HMACSHA1',
  61. 'HMAC-SHA256' => 'Auth_OpenID_HMACSHA256',
  62. ];
  63. /**
  64. * This is an alternate constructor (factory method) used by the
  65. * OpenID consumer library to create associations. OpenID store
  66. * implementations shouldn't use this constructor.
  67. *
  68. * @access private
  69. *
  70. * @param integer $expires_in This is the amount of time this
  71. * association is good for, measured in seconds since the
  72. * association was issued.
  73. *
  74. * @param string $handle This is the handle the server gave this
  75. * association.
  76. *
  77. * @param string $secret This is the shared secret the server
  78. * generated for this association.
  79. *
  80. * @param string $assoc_type This is the type of association this
  81. * instance represents. The only valid values of this field at
  82. * this time is 'HMAC-SHA1' and 'HMAC-SHA256', but new types may
  83. * be defined in the future.
  84. *
  85. * @return Auth_OpenID_Association
  86. */
  87. static function fromExpiresIn($expires_in, $handle, $secret, $assoc_type)
  88. {
  89. $issued = time();
  90. $lifetime = $expires_in;
  91. return new Auth_OpenID_Association($handle, $secret,
  92. $issued, $lifetime, $assoc_type);
  93. }
  94. /**
  95. * This is the standard constructor for creating an association.
  96. * The library should create all of the necessary associations, so
  97. * this constructor is not part of the external API.
  98. *
  99. * @access private
  100. *
  101. * @param string $handle This is the handle the server gave this
  102. * association.
  103. *
  104. * @param string $secret This is the shared secret the server
  105. * generated for this association.
  106. *
  107. * @param integer $issued This is the time this association was
  108. * issued, in seconds since 00:00 GMT, January 1, 1970. (ie, a
  109. * unix timestamp)
  110. *
  111. * @param integer $lifetime This is the amount of time this
  112. * association is good for, measured in seconds since the
  113. * association was issued.
  114. *
  115. * @param string $assoc_type This is the type of association this
  116. * instance represents. The only valid values of this field at
  117. * this time is 'HMAC-SHA1' and 'HMAC-SHA256', but new types may
  118. * be defined in the future.
  119. */
  120. function __construct(
  121. $handle, $secret, $issued, $lifetime, $assoc_type)
  122. {
  123. if (!in_array($assoc_type,
  124. Auth_OpenID_getSupportedAssociationTypes(), true)) {
  125. $fmt = 'Unsupported association type (%s)';
  126. trigger_error(sprintf($fmt, $assoc_type), E_USER_ERROR);
  127. }
  128. $this->handle = $handle;
  129. $this->secret = $secret;
  130. $this->issued = $issued;
  131. $this->lifetime = $lifetime;
  132. $this->assoc_type = $assoc_type;
  133. }
  134. /**
  135. * This returns the number of seconds this association is still
  136. * valid for, or 0 if the association is no longer valid.
  137. *
  138. * @param int|null $now
  139. * @return int $seconds The number of seconds this association
  140. * is still valid for, or 0 if the association is no longer valid.
  141. */
  142. function getExpiresIn($now = null)
  143. {
  144. if ($now == null) {
  145. $now = time();
  146. }
  147. return max(0, $this->issued + $this->lifetime - $now);
  148. }
  149. /**
  150. * This checks to see if two {@link Auth_OpenID_Association}
  151. * instances represent the same association.
  152. *
  153. * @param object $other
  154. * @return bool $result true if the two instances represent the
  155. * same association, false otherwise.
  156. */
  157. function equal($other)
  158. {
  159. return ((gettype($this) == gettype($other))
  160. && ($this->handle == $other->handle)
  161. && ($this->secret == $other->secret)
  162. && ($this->issued == $other->issued)
  163. && ($this->lifetime == $other->lifetime)
  164. && ($this->assoc_type == $other->assoc_type));
  165. }
  166. /**
  167. * Convert an association to KV form.
  168. *
  169. * @return string $result String in KV form suitable for
  170. * deserialization by deserialize.
  171. */
  172. function serialize()
  173. {
  174. $data = [
  175. 'version' => '2',
  176. 'handle' => $this->handle,
  177. 'secret' => base64_encode($this->secret),
  178. 'issued' => strval(intval($this->issued)),
  179. 'lifetime' => strval(intval($this->lifetime)),
  180. 'assoc_type' => $this->assoc_type,
  181. ];
  182. assert(array_keys($data) == $this->assoc_keys);
  183. return Auth_OpenID_KVForm::fromArray($data);
  184. }
  185. /**
  186. * Parse an association as stored by serialize(). This is the
  187. * inverse of serialize.
  188. *
  189. * @param string $class_name
  190. * @param string $assoc_s Association as serialized by serialize()
  191. * @return Auth_OpenID_Association $result instance of this class
  192. */
  193. static function deserialize($class_name, $assoc_s)
  194. {
  195. $pairs = Auth_OpenID_KVForm::toArray($assoc_s, $strict = true);
  196. $keys = [];
  197. $values = [];
  198. foreach ($pairs as $key => $value) {
  199. if (is_array($value)) {
  200. list($key, $value) = $value;
  201. }
  202. $keys[] = $key;
  203. $values[] = $value;
  204. }
  205. $class_vars = get_class_vars($class_name);
  206. $class_assoc_keys = $class_vars['assoc_keys'];
  207. sort($keys);
  208. sort($class_assoc_keys);
  209. if ($keys != $class_assoc_keys) {
  210. trigger_error('Unexpected key values: ' . var_export($keys, true),
  211. E_USER_WARNING);
  212. return null;
  213. }
  214. $version = $pairs['version'];
  215. $handle = $pairs['handle'];
  216. $secret = $pairs['secret'];
  217. $issued = $pairs['issued'];
  218. $lifetime = $pairs['lifetime'];
  219. $assoc_type = $pairs['assoc_type'];
  220. if ($version != '2') {
  221. trigger_error('Unknown version: ' . $version, E_USER_WARNING);
  222. return null;
  223. }
  224. $issued = intval($issued);
  225. $lifetime = intval($lifetime);
  226. $secret = base64_decode($secret);
  227. return new $class_name(
  228. $handle, $secret, $issued, $lifetime, $assoc_type);
  229. }
  230. /**
  231. * Generate a signature for a sequence of (key, value) pairs
  232. *
  233. * @access private
  234. * @param array $pairs The pairs to sign, in order. This is an
  235. * array of two-tuples.
  236. * @return string $signature The binary signature of this sequence
  237. * of pairs
  238. */
  239. function sign($pairs)
  240. {
  241. $kv = Auth_OpenID_KVForm::fromArray($pairs);
  242. /* Invalid association types should be caught at constructor */
  243. $callback = $this->_macs[$this->assoc_type];
  244. return call_user_func_array($callback, [$this->secret, $kv]);
  245. }
  246. /**
  247. * Generate a signature for some fields in a dictionary
  248. *
  249. * @access private
  250. * @param Auth_OpenID_Message $message
  251. * @return string $signature The signature, base64 encoded
  252. * @internal param array $fields The fields to sign, in order; this is an
  253. * array of strings.
  254. * @internal param array $data Dictionary of values to sign (an array of
  255. * string => string pairs).
  256. */
  257. function signMessage($message)
  258. {
  259. if ($message->hasKey(Auth_OpenID_OPENID_NS, 'sig') ||
  260. $message->hasKey(Auth_OpenID_OPENID_NS, 'signed')) {
  261. // Already has a sig
  262. return null;
  263. }
  264. $extant_handle = $message->getArg(Auth_OpenID_OPENID_NS,
  265. 'assoc_handle');
  266. if ($extant_handle && ($extant_handle != $this->handle)) {
  267. // raise ValueError("Message has a different association handle")
  268. return null;
  269. }
  270. $signed_message = $message;
  271. $signed_message->setArg(Auth_OpenID_OPENID_NS, 'assoc_handle',
  272. $this->handle);
  273. $message_keys = array_keys($signed_message->toPostArgs());
  274. $signed_list = [];
  275. $signed_prefix = 'openid.';
  276. foreach ($message_keys as $k) {
  277. if (strpos($k, $signed_prefix) === 0) {
  278. $signed_list[] = substr($k, strlen($signed_prefix));
  279. }
  280. }
  281. $signed_list[] = 'signed';
  282. sort($signed_list);
  283. $signed_message->setArg(Auth_OpenID_OPENID_NS, 'signed',
  284. implode(',', $signed_list));
  285. $sig = $this->getMessageSignature($signed_message);
  286. $signed_message->setArg(Auth_OpenID_OPENID_NS, 'sig', $sig);
  287. return $signed_message;
  288. }
  289. /**
  290. * Given a {@link Auth_OpenID_Message}, return the key/value pairs
  291. * to be signed according to the signed list in the message. If
  292. * the message lacks a signed list, return null.
  293. *
  294. * @access private
  295. * @param Auth_OpenID_Message $message
  296. * @return array|null
  297. */
  298. function _makePairs($message)
  299. {
  300. $signed = $message->getArg(Auth_OpenID_OPENID_NS, 'signed');
  301. if (!$signed || Auth_OpenID::isFailure($signed)) {
  302. // raise ValueError('Message has no signed list: %s' % (message,))
  303. return null;
  304. }
  305. $signed_list = explode(',', $signed);
  306. $pairs = [];
  307. $data = $message->toPostArgs();
  308. foreach ($signed_list as $field) {
  309. $pairs[] = [
  310. $field, Auth_OpenID::arrayGet($data,
  311. 'openid.' .
  312. $field, '')
  313. ];
  314. }
  315. return $pairs;
  316. }
  317. /**
  318. * Given an {@link Auth_OpenID_Message}, return the signature for
  319. * the signed list in the message.
  320. *
  321. * @access private
  322. * @param Auth_OpenID_Message $message
  323. * @return string
  324. */
  325. function getMessageSignature($message)
  326. {
  327. $pairs = $this->_makePairs($message);
  328. return base64_encode($this->sign($pairs));
  329. }
  330. /**
  331. * Confirm that the signature of these fields matches the
  332. * signature contained in the data.
  333. *
  334. * @access private
  335. * @param Auth_OpenID_Message $message
  336. * @return bool
  337. */
  338. function checkMessageSignature($message)
  339. {
  340. $sig = $message->getArg(Auth_OpenID_OPENID_NS,
  341. 'sig');
  342. if (!$sig || Auth_OpenID::isFailure($sig)) {
  343. return false;
  344. }
  345. $calculated_sig = $this->getMessageSignature($message);
  346. return Auth_OpenID_CryptUtil::constEq($calculated_sig, $sig);
  347. }
  348. }
  349. function Auth_OpenID_getSecretSize($assoc_type)
  350. {
  351. if ($assoc_type == 'HMAC-SHA1') {
  352. return 20;
  353. } else if ($assoc_type == 'HMAC-SHA256') {
  354. return 32;
  355. } else {
  356. return null;
  357. }
  358. }
  359. function Auth_OpenID_getAllAssociationTypes()
  360. {
  361. return ['HMAC-SHA1', 'HMAC-SHA256'];
  362. }
  363. function Auth_OpenID_getSupportedAssociationTypes()
  364. {
  365. $a = ['HMAC-SHA1'];
  366. if (Auth_OpenID_HMACSHA256_SUPPORTED) {
  367. $a[] = 'HMAC-SHA256';
  368. }
  369. return $a;
  370. }
  371. /**
  372. * @param string $assoc_type
  373. * @return mixed
  374. */
  375. function Auth_OpenID_getSessionTypes($assoc_type)
  376. {
  377. $assoc_to_session = [
  378. 'HMAC-SHA1' => ['DH-SHA1', 'no-encryption']
  379. ];
  380. if (Auth_OpenID_HMACSHA256_SUPPORTED) {
  381. $assoc_to_session['HMAC-SHA256'] =
  382. ['DH-SHA256', 'no-encryption'];
  383. }
  384. return Auth_OpenID::arrayGet($assoc_to_session, $assoc_type, []);
  385. }
  386. function Auth_OpenID_checkSessionType($assoc_type, $session_type)
  387. {
  388. if (!in_array($session_type,
  389. Auth_OpenID_getSessionTypes($assoc_type))) {
  390. return false;
  391. }
  392. return true;
  393. }
  394. function Auth_OpenID_getDefaultAssociationOrder()
  395. {
  396. $order = [];
  397. if (!Auth_OpenID_noMathSupport()) {
  398. $order[] = ['HMAC-SHA1', 'DH-SHA1'];
  399. if (Auth_OpenID_HMACSHA256_SUPPORTED) {
  400. $order[] = ['HMAC-SHA256', 'DH-SHA256'];
  401. }
  402. }
  403. $order[] = ['HMAC-SHA1', 'no-encryption'];
  404. if (Auth_OpenID_HMACSHA256_SUPPORTED) {
  405. $order[] = ['HMAC-SHA256', 'no-encryption'];
  406. }
  407. return $order;
  408. }
  409. function Auth_OpenID_getOnlyEncryptedOrder()
  410. {
  411. $result = [];
  412. foreach (Auth_OpenID_getDefaultAssociationOrder() as $pair) {
  413. list($assoc, $session) = $pair;
  414. if ($session != 'no-encryption') {
  415. if (Auth_OpenID_HMACSHA256_SUPPORTED &&
  416. ($assoc == 'HMAC-SHA256')) {
  417. $result[] = $pair;
  418. } else if ($assoc != 'HMAC-SHA256') {
  419. $result[] = $pair;
  420. }
  421. }
  422. }
  423. return $result;
  424. }
  425. function Auth_OpenID_getDefaultNegotiator()
  426. {
  427. return new Auth_OpenID_SessionNegotiator(
  428. Auth_OpenID_getDefaultAssociationOrder());
  429. }
  430. function Auth_OpenID_getEncryptedNegotiator()
  431. {
  432. return new Auth_OpenID_SessionNegotiator(
  433. Auth_OpenID_getOnlyEncryptedOrder());
  434. }
  435. /**
  436. * A session negotiator controls the allowed and preferred association
  437. * types and association session types. Both the {@link
  438. * Auth_OpenID_Consumer} and {@link Auth_OpenID_Server} use
  439. * negotiators when creating associations.
  440. *
  441. * You can create and use negotiators if you:
  442. * - Do not want to do Diffie-Hellman key exchange because you use
  443. * transport-layer encryption (e.g. SSL)
  444. *
  445. * - Want to use only SHA-256 associations
  446. *
  447. * - Do not want to support plain-text associations over a non-secure
  448. * channel
  449. *
  450. * It is up to you to set a policy for what kinds of associations to
  451. * accept. By default, the library will make any kind of association
  452. * that is allowed in the OpenID 2.0 specification.
  453. *
  454. * Use of negotiators in the library
  455. * =================================
  456. *
  457. * When a consumer makes an association request, it calls {@link
  458. * getAllowedType} to get the preferred association type and
  459. * association session type.
  460. *
  461. * The server gets a request for a particular association/session type
  462. * and calls {@link isAllowed} to determine if it should create an
  463. * association. If it is supported, negotiation is complete. If it is
  464. * not, the server calls {@link getAllowedType} to get an allowed
  465. * association type to return to the consumer.
  466. *
  467. * If the consumer gets an error response indicating that the
  468. * requested association/session type is not supported by the server
  469. * that contains an assocation/session type to try, it calls {@link
  470. * isAllowed} to determine if it should try again with the given
  471. * combination of association/session type.
  472. *
  473. * @package OpenID
  474. */
  475. class Auth_OpenID_SessionNegotiator {
  476. function __construct($allowed_types)
  477. {
  478. $this->allowed_types = [];
  479. $this->setAllowedTypes($allowed_types);
  480. }
  481. /**
  482. * Set the allowed association types, checking to make sure each
  483. * combination is valid.
  484. *
  485. * @access private
  486. * @param array $allowed_types
  487. * @return bool
  488. */
  489. function setAllowedTypes($allowed_types)
  490. {
  491. foreach ($allowed_types as $pair) {
  492. list($assoc_type, $session_type) = $pair;
  493. if (!Auth_OpenID_checkSessionType($assoc_type, $session_type)) {
  494. return false;
  495. }
  496. }
  497. $this->allowed_types = $allowed_types;
  498. return true;
  499. }
  500. /**
  501. * Add an association type and session type to the allowed types
  502. * list. The assocation/session pairs are tried in the order that
  503. * they are added.
  504. *
  505. * @access private
  506. * @param $assoc_type
  507. * @param null $session_type
  508. * @return bool
  509. */
  510. function addAllowedType($assoc_type, $session_type = null)
  511. {
  512. if ($this->allowed_types === null) {
  513. $this->allowed_types = [];
  514. }
  515. if ($session_type === null) {
  516. $available = Auth_OpenID_getSessionTypes($assoc_type);
  517. if (!$available) {
  518. return false;
  519. }
  520. foreach ($available as $session_type) {
  521. $this->addAllowedType($assoc_type, $session_type);
  522. }
  523. } else {
  524. if (Auth_OpenID_checkSessionType($assoc_type, $session_type)) {
  525. $this->allowed_types[] = [$assoc_type, $session_type];
  526. } else {
  527. return false;
  528. }
  529. }
  530. return true;
  531. }
  532. // Is this combination of association type and session type allowed?
  533. function isAllowed($assoc_type, $session_type)
  534. {
  535. $assoc_good = in_array([$assoc_type, $session_type],
  536. $this->allowed_types);
  537. $matches = in_array($session_type,
  538. Auth_OpenID_getSessionTypes($assoc_type));
  539. return ($assoc_good && $matches);
  540. }
  541. /**
  542. * Get a pair of assocation type and session type that are
  543. * supported.
  544. */
  545. function getAllowedType()
  546. {
  547. if (!$this->allowed_types) {
  548. return [null, null];
  549. }
  550. return $this->allowed_types[0];
  551. }
  552. }