PageRenderTime 51ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/sources/subs/OpenID.subs.php

https://github.com/Arantor/Elkarte
PHP | 658 lines | 399 code | 119 blank | 140 comment | 83 complexity | c452e86b53e21aa1f790223bbab5dae9 MD5 | raw file
Possible License(s): BSD-3-Clause, LGPL-3.0
  1. <?php
  2. /**
  3. * @name ElkArte Forum
  4. * @copyright ElkArte Forum contributors
  5. * @license BSD http://opensource.org/licenses/BSD-3-Clause
  6. *
  7. * This software is a derived product, based on:
  8. *
  9. * Simple Machines Forum (SMF)
  10. * copyright: 2011 Simple Machines (http://www.simplemachines.org)
  11. * license: BSD, See included LICENSE.TXT for terms and conditions.
  12. *
  13. * @version 1.0 Alpha
  14. *
  15. * Handle all of the OpenID interfacing and communications.
  16. *
  17. */
  18. if (!defined('ELKARTE'))
  19. die('No access...');
  20. /**
  21. * Openid_uri is the URI given by the user
  22. * Validates the URI and changes it to a fully canonical URL
  23. * Determines the IDP server and delegation
  24. * Optional array of fields to restore when validation complete.
  25. * Redirects the user to the IDP for validation
  26. *
  27. * @param string $openid_uri
  28. * @param bool $return = false
  29. * @param array $save_fields = array()
  30. * @param string $return_action = null
  31. * @return string
  32. */
  33. function openID_validate($openid_uri, $return = false, $save_fields = array(), $return_action = null)
  34. {
  35. global $scripturl, $boardurl, $modSettings;
  36. $openid_url = openID_canonize($openid_uri);
  37. $response_data = openID_getServerInfo($openid_url);
  38. if ($response_data === false)
  39. return 'no_data';
  40. if (($assoc = openID_getAssociation($response_data['server'])) == null)
  41. $assoc = openID_makeAssociation($response_data['server']);
  42. // Before we go wherever it is we are going, store the GET and POST data, because it might be useful when we get back.
  43. $request_time = time();
  44. // Just in case they are doing something else at this time.
  45. while (isset($_SESSION['openid']['saved_data'][$request_time]))
  46. $request_time = md5($request_time);
  47. $_SESSION['openid']['saved_data'][$request_time] = array(
  48. 'get' => $_GET,
  49. 'post' => $_POST,
  50. 'openid_uri' => $openid_url,
  51. 'cookieTime' => $modSettings['cookieTime'],
  52. );
  53. $parameters = array(
  54. 'openid.mode=checkid_setup',
  55. 'openid.trust_root=' . urlencode($scripturl),
  56. 'openid.identity=' . urlencode(empty($response_data['delegate']) ? $openid_url : $response_data['delegate']),
  57. 'openid.assoc_handle=' . urlencode($assoc['handle']),
  58. 'openid.return_to=' . urlencode($scripturl . '?action=openidreturn&sa=' . (!empty($return_action) ? $return_action : $_REQUEST['action']) . '&t=' . $request_time . (!empty($save_fields) ? '&sf=' . base64_encode(serialize($save_fields)) : '')),
  59. );
  60. // If they are logging in but don't yet have an account or they are registering, let's request some additional information
  61. if (($_REQUEST['action'] == 'login2' && !openid_member_exists($openid_url)) || ($_REQUEST['action'] == 'register' || $_REQUEST['action'] == 'register2'))
  62. {
  63. // Email is required.
  64. $parameters[] = 'openid.sreg.required=email';
  65. // The rest is just optional.
  66. $parameters[] = 'openid.sreg.optional=nickname,dob,gender';
  67. }
  68. $redir_url = $response_data['server'] . '?' . implode('&', $parameters);
  69. if ($return)
  70. return $redir_url;
  71. else
  72. redirectexit($redir_url);
  73. }
  74. /**
  75. * Revalidate a user using OpenID.
  76. * Note that this function will not return when authentication is required.
  77. *
  78. * @return boolean
  79. */
  80. function openID_revalidate()
  81. {
  82. global $user_settings;
  83. if (isset($_SESSION['openid_revalidate_time']) && $_SESSION['openid_revalidate_time'] > time() - 60)
  84. {
  85. unset($_SESSION['openid_revalidate_time']);
  86. return true;
  87. }
  88. else
  89. openID_validate($user_settings['openid_uri'], false, null, 'revalidate');
  90. // We shouldn't get here.
  91. trigger_error('Hacking attempt...', E_USER_ERROR);
  92. }
  93. /**
  94. * Retrieve an existing, not expired, association if there is any.
  95. *
  96. * @param string $server
  97. * @param string $handle = null
  98. * @param bool $no_delete = false
  99. * @return array
  100. */
  101. function openID_getAssociation($server, $handle = null, $no_delete = false)
  102. {
  103. global $smcFunc;
  104. if (!$no_delete)
  105. {
  106. // Delete the already expired associations.
  107. $smcFunc['db_query']('openid_delete_assoc_old', '
  108. DELETE FROM {db_prefix}openid_assoc
  109. WHERE expires <= {int:current_time}',
  110. array(
  111. 'current_time' => time(),
  112. )
  113. );
  114. }
  115. // Get the association that has the longest lifetime from now.
  116. $request = $smcFunc['db_query']('openid_select_assoc', '
  117. SELECT server_url, handle, secret, issued, expires, assoc_type
  118. FROM {db_prefix}openid_assoc
  119. WHERE server_url = {string:server_url}' . ($handle === null ? '' : '
  120. AND handle = {string:handle}') . '
  121. ORDER BY expires DESC',
  122. array(
  123. 'server_url' => $server,
  124. 'handle' => $handle,
  125. )
  126. );
  127. if ($smcFunc['db_num_rows']($request) == 0)
  128. return null;
  129. $return = $smcFunc['db_fetch_assoc']($request);
  130. $smcFunc['db_free_result']($request);
  131. return $return;
  132. }
  133. /**
  134. * Create and store an association to the given server.
  135. *
  136. * @param string $server
  137. * @return array
  138. */
  139. function openID_makeAssociation($server)
  140. {
  141. global $smcFunc, $modSettings, $p;
  142. $parameters = array(
  143. 'openid.mode=associate',
  144. );
  145. // We'll need to get our keys for the Diffie-Hellman key exchange.
  146. $dh_keys = openID_setup_DH();
  147. // If we don't support DH we'll have to see if the provider will accept no encryption.
  148. if ($dh_keys === false)
  149. $parameters[] = 'openid.session_type=';
  150. else
  151. {
  152. $parameters[] = 'openid.session_type=DH-SHA1';
  153. $parameters[] = 'openid.dh_consumer_public=' . urlencode(base64_encode(long_to_binary($dh_keys['public'])));
  154. $parameters[] = 'openid.assoc_type=HMAC-SHA1';
  155. }
  156. // The data to post to the server.
  157. $post_data = implode('&', $parameters);
  158. $data = fetch_web_data($server, $post_data);
  159. // Parse the data given.
  160. preg_match_all('~^([^:]+):(.+)$~m', $data, $matches);
  161. $assoc_data = array();
  162. foreach ($matches[1] as $key => $match)
  163. $assoc_data[$match] = $matches[2][$key];
  164. if (!isset($assoc_data['assoc_type']) || (empty($assoc_data['mac_key']) && empty($assoc_data['enc_mac_key'])))
  165. fatal_lang_error('openid_server_bad_response');
  166. // Clean things up a bit.
  167. $handle = isset($assoc_data['assoc_handle']) ? $assoc_data['assoc_handle'] : '';
  168. $issued = time();
  169. $expires = $issued + min((int)$assoc_data['expires_in'], 60);
  170. $assoc_type = isset($assoc_data['assoc_type']) ? $assoc_data['assoc_type'] : '';
  171. // @todo Is this really needed?
  172. foreach (array('dh_server_public', 'enc_mac_key') as $key)
  173. if (isset($assoc_data[$key]))
  174. $assoc_data[$key] = str_replace(' ', '+', $assoc_data[$key]);
  175. // Figure out the Diffie-Hellman secret.
  176. if (!empty($assoc_data['enc_mac_key']))
  177. {
  178. $dh_secret = bcpowmod(binary_to_long(base64_decode($assoc_data['dh_server_public'])), $dh_keys['private'], $p);
  179. $secret = base64_encode(binary_xor(sha1(long_to_binary($dh_secret), true), base64_decode($assoc_data['enc_mac_key'])));
  180. }
  181. else
  182. $secret = $assoc_data['mac_key'];
  183. // Store the data
  184. $smcFunc['db_insert']('replace',
  185. '{db_prefix}openid_assoc',
  186. array('server_url' => 'string', 'handle' => 'string', 'secret' => 'string', 'issued' => 'int', 'expires' => 'int', 'assoc_type' => 'string'),
  187. array($server, $handle, $secret, $issued, $expires, $assoc_type),
  188. array('server_url', 'handle')
  189. );
  190. return array(
  191. 'server' => $server,
  192. 'handle' => $assoc_data['assoc_handle'],
  193. 'secret' => $secret,
  194. 'issued' => $issued,
  195. 'expires' => $expires,
  196. 'assoc_type' => $assoc_data['assoc_type'],
  197. );
  198. }
  199. /**
  200. * Delete an existing association from the database.
  201. *
  202. * @param string $handle
  203. */
  204. function openID_removeAssociation($handle)
  205. {
  206. global $smcFunc;
  207. $smcFunc['db_query']('openid_remove_association', '
  208. DELETE FROM {db_prefix}openid_assoc
  209. WHERE handle = {string:handle}',
  210. array(
  211. 'handle' => $handle,
  212. )
  213. );
  214. }
  215. /**
  216. * Callback action handler for OpenID
  217. */
  218. function action_openidreturn()
  219. {
  220. global $smcFunc, $user_info, $user_profile, $modSettings, $context, $sc, $user_settings;
  221. // Is OpenID even enabled?
  222. if (empty($modSettings['enableOpenID']))
  223. fatal_lang_error('no_access', false);
  224. if (!isset($_GET['openid_mode']))
  225. fatal_lang_error('openid_return_no_mode', false);
  226. // @todo Check for error status!
  227. if ($_GET['openid_mode'] != 'id_res')
  228. fatal_lang_error('openid_not_resolved');
  229. // this has annoying habit of removing the + from the base64 encoding. So lets put them back.
  230. foreach (array('openid_assoc_handle', 'openid_invalidate_handle', 'openid_sig', 'sf') as $key)
  231. if (isset($_GET[$key]))
  232. $_GET[$key] = str_replace(' ', '+', $_GET[$key]);
  233. // Did they tell us to remove any associations?
  234. if (!empty($_GET['openid_invalidate_handle']))
  235. openid_removeAssociation($_GET['openid_invalidate_handle']);
  236. $server_info = openid_getServerInfo($_GET['openid_identity']);
  237. // Get the association data.
  238. $assoc = openID_getAssociation($server_info['server'], $_GET['openid_assoc_handle'], true);
  239. if ($assoc === null)
  240. fatal_lang_error('openid_no_assoc');
  241. $secret = base64_decode($assoc['secret']);
  242. $signed = explode(',', $_GET['openid_signed']);
  243. $verify_str = '';
  244. foreach ($signed as $sign)
  245. {
  246. $verify_str .= $sign . ':' . strtr($_GET['openid_' . str_replace('.', '_', $sign)], array('&amp;' => '&')) . "\n";
  247. }
  248. $verify_str = base64_encode(sha1_hmac($verify_str, $secret));
  249. if ($verify_str != $_GET['openid_sig'])
  250. {
  251. fatal_lang_error('openid_sig_invalid', 'critical');
  252. }
  253. if (!isset($_SESSION['openid']['saved_data'][$_GET['t']]))
  254. fatal_lang_error('openid_load_data');
  255. $openid_uri = $_SESSION['openid']['saved_data'][$_GET['t']]['openid_uri'];
  256. $modSettings['cookieTime'] = $_SESSION['openid']['saved_data'][$_GET['t']]['cookieTime'];
  257. if (empty($openid_uri))
  258. fatal_lang_error('openid_load_data');
  259. // Any save fields to restore?
  260. $context['openid_save_fields'] = isset($_GET['sf']) ? unserialize(base64_decode($_GET['sf'])) : array();
  261. // Is there a user with this OpenID_uri?
  262. $result = $smcFunc['db_query']('', '
  263. SELECT passwd, id_member, id_group, lngfile, is_activated, email_address, additional_groups, member_name, password_salt,
  264. openid_uri
  265. FROM {db_prefix}members
  266. WHERE openid_uri = {string:openid_uri}',
  267. array(
  268. 'openid_uri' => $openid_uri,
  269. )
  270. );
  271. $member_found = $smcFunc['db_num_rows']($result);
  272. if (!$member_found && isset($_GET['sa']) && $_GET['sa'] == 'change_uri' && !empty($_SESSION['new_openid_uri']) && $_SESSION['new_openid_uri'] == $openid_uri)
  273. {
  274. // Update the member.
  275. updateMemberData($user_settings['id_member'], array('openid_uri' => $openid_uri));
  276. unset($_SESSION['new_openid_uri']);
  277. $_SESSION['openid'] = array(
  278. 'verified' => true,
  279. 'openid_uri' => $openid_uri,
  280. );
  281. // Send them back to profile.
  282. redirectexit('action=profile;area=authentication;updated');
  283. }
  284. elseif (!$member_found)
  285. {
  286. // Store the received openid info for the user when returned to the registration page.
  287. $_SESSION['openid'] = array(
  288. 'verified' => true,
  289. 'openid_uri' => $openid_uri,
  290. );
  291. if (isset($_GET['openid_sreg_nickname']))
  292. $_SESSION['openid']['nickname'] = $_GET['openid_sreg_nickname'];
  293. if (isset($_GET['openid_sreg_email']))
  294. $_SESSION['openid']['email'] = $_GET['openid_sreg_email'];
  295. if (isset($_GET['openid_sreg_dob']))
  296. $_SESSION['openid']['dob'] = $_GET['openid_sreg_dob'];
  297. if (isset($_GET['openid_sreg_gender']))
  298. $_SESSION['openid']['gender'] = $_GET['openid_sreg_gender'];
  299. // Were we just verifying the registration state?
  300. if (isset($_GET['sa']) && $_GET['sa'] == 'register2')
  301. {
  302. require_once(CONTROLLERDIR . '/Register.controller.php');
  303. return action_register2(true);
  304. }
  305. else
  306. redirectexit('action=register');
  307. }
  308. elseif (isset($_GET['sa']) && $_GET['sa'] == 'revalidate' && $user_settings['openid_uri'] == $openid_uri)
  309. {
  310. $_SESSION['openid_revalidate_time'] = time();
  311. // Restore the get data.
  312. require_once(SUBSDIR . '/Auth.subs.php');
  313. $_SESSION['openid']['saved_data'][$_GET['t']]['get']['openid_restore_post'] = $_GET['t'];
  314. $query_string = construct_query_string($_SESSION['openid']['saved_data'][$_GET['t']]['get']);
  315. redirectexit($query_string);
  316. }
  317. else
  318. {
  319. $user_settings = $smcFunc['db_fetch_assoc']($result);
  320. $smcFunc['db_free_result']($result);
  321. $user_settings['passwd'] = sha1(strtolower($user_settings['member_name']) . $secret);
  322. $user_settings['password_salt'] = substr(md5(mt_rand()), 0, 4);
  323. updateMemberData($user_settings['id_member'], array('passwd' => $user_settings['passwd'], 'password_salt' => $user_settings['password_salt']));
  324. // Cleanup on Aisle 5.
  325. $_SESSION['openid'] = array(
  326. 'verified' => true,
  327. 'openid_uri' => $openid_uri,
  328. );
  329. require_once(CONTROLLERDIR . '/LogInOut.controller.php');
  330. if (!checkActivation())
  331. return;
  332. DoLogin();
  333. }
  334. }
  335. /**
  336. * Fix the URI to a canonical form
  337. *
  338. * @param string $uri
  339. */
  340. function openID_canonize($uri)
  341. {
  342. // @todo Add in discovery.
  343. if (strpos($uri, 'http://') !== 0 && strpos($uri, 'https://') !== 0)
  344. $uri = 'http://' . $uri;
  345. if (strpos(substr($uri, strpos($uri, '://') + 3), '/') === false)
  346. $uri .= '/';
  347. return $uri;
  348. }
  349. /**
  350. * Check if the URI is already registered for an existing member
  351. *
  352. * @param string $uri
  353. * @return array
  354. */
  355. function openid_member_exists($url)
  356. {
  357. global $smcFunc;
  358. $request = $smcFunc['db_query']('openid_member_exists', '
  359. SELECT mem.id_member, mem.member_name
  360. FROM {db_prefix}members AS mem
  361. WHERE mem.openid_uri = {string:openid_uri}',
  362. array(
  363. 'openid_uri' => $url,
  364. )
  365. );
  366. $member = $smcFunc['db_fetch_assoc']($request);
  367. $smcFunc['db_free_result']($request);
  368. return $member;
  369. }
  370. /**
  371. * Prepare for a Diffie-Hellman key exchange.
  372. * @param bool $regenerate = false
  373. * @return array|false return false on failure or an array() on success
  374. */
  375. function openID_setup_DH($regenerate = false)
  376. {
  377. global $p, $g;
  378. // First off, do we have BC Math available?
  379. if (!function_exists('bcpow'))
  380. return false;
  381. // Defined in OpenID spec.
  382. $p = '155172898181473697471232257763715539915724801966915404479707795314057629378541917580651227423698188993727816152646631438561595825688188889951272158842675419950341258706556549803580104870537681476726513255747040765857479291291572334510643245094715007229621094194349783925984760375594985848253359305585439638443';
  383. $g = '2';
  384. // Make sure the scale is set.
  385. bcscale(0);
  386. return openID_get_keys($regenerate);
  387. }
  388. /**
  389. * Retrieve DH keys from the store.
  390. * It generates them if they're not stored or $regerate parameter is true.
  391. *
  392. * @param bool $regenerate
  393. */
  394. function openID_get_keys($regenerate)
  395. {
  396. global $modSettings, $p, $g;
  397. // Ok lets take the easy way out, are their any keys already defined for us? They are changed in the daily maintenance scheduled task.
  398. if (!empty($modSettings['dh_keys']) && !$regenerate)
  399. {
  400. // Sweeeet!
  401. list ($public, $private) = explode("\n", $modSettings['dh_keys']);
  402. return array(
  403. 'public' => base64_decode($public),
  404. 'private' => base64_decode($private),
  405. );
  406. }
  407. // Dang it, now I have to do math. And it's not just ordinary math, its the evil big interger math. This will take a few seconds.
  408. $private = openid_generate_private_key();
  409. $public = bcpowmod($g, $private, $p);
  410. // Now that we did all that work, lets save it so we don't have to keep doing it.
  411. $keys = array('dh_keys' => base64_encode($public) . "\n" . base64_encode($private));
  412. updateSettings($keys);
  413. return array(
  414. 'public' => $public,
  415. 'private' => $private,
  416. );
  417. }
  418. /**
  419. * Generate private key
  420. *
  421. * @return float
  422. */
  423. function openid_generate_private_key()
  424. {
  425. global $p;
  426. static $cache = array();
  427. $byte_string = long_to_binary($p);
  428. if (isset($cache[$byte_string]))
  429. list ($dup, $num_bytes) = $cache[$byte_string];
  430. else
  431. {
  432. $num_bytes = strlen($byte_string) - ($byte_string[0] == "\x00" ? 1 : 0);
  433. $max_rand = bcpow(256, $num_bytes);
  434. $dup = bcmod($max_rand, $num_bytes);
  435. $cache[$byte_string] = array($dup, $num_bytes);
  436. }
  437. do
  438. {
  439. $str = '';
  440. for ($i = 0; $i < $num_bytes; $i += 4)
  441. $str .= pack('L', mt_rand());
  442. $bytes = "\x00" . $str;
  443. $num = binary_to_long($bytes);
  444. } while (bccomp($num, $dup) < 0);
  445. return bcadd(bcmod($num, $p), 1);
  446. }
  447. /**
  448. *
  449. * Retrieve server information.
  450. *
  451. * @param string $openid_url
  452. * @return boolean|array
  453. */
  454. function openID_getServerInfo($openid_url)
  455. {
  456. require_once(SUBSDIR . '/Package.subs.php');
  457. // Get the html and parse it for the openid variable which will tell us where to go.
  458. $webdata = fetch_web_data($openid_url);
  459. if (empty($webdata))
  460. return false;
  461. $response_data = array();
  462. // Some OpenID servers have strange but still valid HTML which makes our job hard.
  463. if (preg_match_all('~<link([\s\S]*?)/?>~i', $webdata, $link_matches) == 0)
  464. fatal_lang_error('openid_server_bad_response');
  465. foreach ($link_matches[1] as $link_match)
  466. {
  467. if (preg_match('~rel="([\s\S]*?)"~i', $link_match, $rel_match) == 0 || preg_match('~href="([\s\S]*?)"~i', $link_match, $href_match) == 0)
  468. continue;
  469. $rels = preg_split('~\s+~', $rel_match[1]);
  470. foreach ($rels as $rel)
  471. if (preg_match('~openid2?\.(server|delegate|provider)~i', $rel, $match) != 0)
  472. $response_data[$match[1]] = $href_match[1];
  473. }
  474. if (empty($response_data['server']))
  475. if (empty($response_data['provider']))
  476. fatal_lang_error('openid_server_bad_response');
  477. else
  478. $response_data['server'] = $response_data['provider'];
  479. return $response_data;
  480. }
  481. /**
  482. * @param string $data
  483. * @param string $key
  484. * @return string
  485. */
  486. function sha1_hmac($data, $key)
  487. {
  488. if (strlen($key) > 64)
  489. $key = sha1($key, true);
  490. // Pad the key if need be.
  491. $key = str_pad($key, 64, chr(0x00));
  492. $ipad = str_repeat(chr(0x36), 64);
  493. $opad = str_repeat(chr(0x5c), 64);
  494. $hash1 = sha1(($key ^ $ipad) . $data, true);
  495. $hmac = sha1(($key ^ $opad) . $hash1, true);
  496. return $hmac;
  497. }
  498. function binary_to_long($str)
  499. {
  500. $bytes = array_merge(unpack('C*', $str));
  501. $n = 0;
  502. foreach ($bytes as $byte)
  503. {
  504. $n = bcmul($n, 256);
  505. $n = bcadd($n, $byte);
  506. }
  507. return $n;
  508. }
  509. function long_to_binary($value)
  510. {
  511. $cmp = bccomp($value, 0);
  512. if ($cmp < 0)
  513. fatal_error('Only non-negative integers allowed.');
  514. if ($cmp == 0)
  515. return "\x00";
  516. $bytes = array();
  517. while (bccomp($value, 0) > 0)
  518. {
  519. array_unshift($bytes, bcmod($value, 256));
  520. $value = bcdiv($value, 256);
  521. }
  522. if ($bytes && ($bytes[0] > 127))
  523. array_unshift($bytes, 0);
  524. $return = '';
  525. foreach ($bytes as $byte)
  526. $return .= pack('C', $byte);
  527. return $return;
  528. }
  529. /**
  530. * @param int $num1
  531. * @param int $num2
  532. */
  533. function binary_xor($num1, $num2)
  534. {
  535. $return = '';
  536. for ($i = 0; $i < strlen($num2); $i++)
  537. $return .= $num1[$i] ^ $num2[$i];
  538. return $return;
  539. }