PageRenderTime 57ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/forum/Sources/Subs-OpenID.php

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