Subs-OpenID.php 18 KB

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