diff forum/Sources/Subs-OpenID.php @ 76:e3e11437ecea website

Add forum code
author Chris Cannam
date Sun, 07 Jul 2013 11:25:48 +0200
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/forum/Sources/Subs-OpenID.php	Sun Jul 07 11:25:48 2013 +0200
@@ -0,0 +1,609 @@
+<?php
+
+/**
+ * Simple Machines Forum (SMF)
+ *
+ * @package SMF
+ * @author Simple Machines http://www.simplemachines.org
+ * @copyright 2011 Simple Machines
+ * @license http://www.simplemachines.org/about/smf/license.php BSD
+ *
+ * @version 2.0
+ */
+
+if (!defined('SMF'))
+	die('Hacking attempt...');
+
+/*	This file handles all of the OpenID interfacing and communications.
+	void smf_openID_validate(string openid_url, bool allow_immediate_validation = true)
+		- openid_uri is the URI given by the user
+		- Validates the URI and changes it to a fully canonicalize URL
+		- Determines the IDP server and delegation
+		- optional array of fields to restore when validation complete.
+		- Redirects the user to the IDP for validation
+*/
+
+function smf_openID_validate($openid_uri, $return = false, $save_fields = array(), $return_action = null)
+{
+	global $sourcedir, $scripturl, $boardurl, $modSettings;
+
+	$openid_url = smf_openID_canonize($openid_uri);
+
+	$response_data = smf_openID_getServerInfo($openid_url);
+	if ($response_data === false)
+		return 'no_data';
+
+	if (($assoc = smf_openID_getAssociation($response_data['server'])) == null)
+		$assoc = smf_openID_makeAssociation($response_data['server']);
+
+	// Before we go wherever it is we are going, store the GET and POST data, because it might be useful when we get back.
+	$request_time = time();
+	// Just in case they are doing something else at this time.
+	while (isset($_SESSION['openid']['saved_data'][$request_time]))
+		$request_time = md5($request_time);
+
+	$_SESSION['openid']['saved_data'][$request_time] = array(
+		'get' => $_GET,
+		'post' => $_POST,
+		'openid_uri' => $openid_url,
+		'cookieTime' => $modSettings['cookieTime'],
+	);
+
+	$parameters = array(
+		'openid.mode=checkid_setup',
+		'openid.trust_root=' . urlencode($scripturl),
+		'openid.identity=' . urlencode(empty($response_data['delegate']) ? $openid_url : $response_data['delegate']),
+		'openid.assoc_handle=' . urlencode($assoc['handle']),
+		'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)) : '')),
+	);
+
+	// If they are logging in but don't yet have an account or they are registering, let's request some additional information
+	if (($_REQUEST['action'] == 'login2' && !smf_openid_member_exists($openid_url)) || ($_REQUEST['action'] == 'register' || $_REQUEST['action'] == 'register2'))
+	{
+		// Email is required.
+		$parameters[] = 'openid.sreg.required=email';
+		// The rest is just optional.
+		$parameters[] = 'openid.sreg.optional=nickname,dob,gender';
+	}
+
+	$redir_url = $response_data['server'] . '?' . implode('&', $parameters);
+
+	if ($return)
+		return $redir_url;
+	else
+		redirectexit($redir_url);
+}
+
+// Revalidate a user using OpenID. Note that this function will not return when authentication is required.
+function smf_openID_revalidate()
+{
+	global $user_settings;
+
+	if (isset($_SESSION['openid_revalidate_time']) && $_SESSION['openid_revalidate_time'] > time() - 60)
+	{
+		unset($_SESSION['openid_revalidate_time']);
+		return true;
+	}
+	else
+		smf_openID_validate($user_settings['openid_uri'], false, null, 'revalidate');
+
+	// We shouldn't get here.
+	trigger_error('Hacking attempt...', E_USER_ERROR);
+}
+
+function smf_openID_getAssociation($server, $handle = null, $no_delete = false)
+{
+	global $smcFunc;
+
+	if (!$no_delete)
+	{
+		// Delete the already expired associations.
+		$smcFunc['db_query']('openid_delete_assoc_old', '
+			DELETE FROM {db_prefix}openid_assoc
+			WHERE expires <= {int:current_time}',
+			array(
+				'current_time' => time(),
+			)
+		);
+	}
+
+	// Get the association that has the longest lifetime from now.
+	$request = $smcFunc['db_query']('openid_select_assoc', '
+		SELECT server_url, handle, secret, issued, expires, assoc_type
+		FROM {db_prefix}openid_assoc
+		WHERE server_url = {string:server_url}' . ($handle === null ? '' : '
+			AND handle = {string:handle}') . '
+		ORDER BY expires DESC',
+		array(
+			'server_url' => $server,
+			'handle' => $handle,
+		)
+	);
+
+	if ($smcFunc['db_num_rows']($request) == 0)
+		return null;
+
+	$return = $smcFunc['db_fetch_assoc']($request);
+	$smcFunc['db_free_result']($request);
+
+	return $return;
+}
+
+function smf_openID_makeAssociation($server)
+{
+	global $smcFunc, $modSettings, $p;
+
+	$parameters = array(
+		'openid.mode=associate',
+	);
+
+	// We'll need to get our keys for the Diffie-Hellman key exchange.
+	$dh_keys = smf_openID_setup_DH();
+
+	// If we don't support DH we'll have to see if the provider will accept no encryption.
+	if ($dh_keys === false)
+		$parameters[] = 'openid.session_type=';
+	else
+	{
+		$parameters[] = 'openid.session_type=DH-SHA1';
+		$parameters[] = 'openid.dh_consumer_public=' . urlencode(base64_encode(long_to_binary($dh_keys['public'])));
+		$parameters[] = 'openid.assoc_type=HMAC-SHA1';
+	}
+
+	// The data to post to the server.
+	$post_data = implode('&', $parameters);
+	$data = fetch_web_data($server, $post_data);
+
+	// Parse the data given.
+	preg_match_all('~^([^:]+):(.+)$~m', $data, $matches);
+	$assoc_data = array();
+
+	foreach ($matches[1] as $key => $match)
+		$assoc_data[$match] = $matches[2][$key];
+
+	if (!isset($assoc_data['assoc_type']) || (empty($assoc_data['mac_key']) && empty($assoc_data['enc_mac_key'])))
+		fatal_lang_error('openid_server_bad_response');
+
+	// Clean things up a bit.
+	$handle = isset($assoc_data['assoc_handle']) ? $assoc_data['assoc_handle'] : '';
+	$issued = time();
+	$expires = $issued + min((int)$assoc_data['expires_in'], 60);
+	$assoc_type = isset($assoc_data['assoc_type']) ? $assoc_data['assoc_type'] : '';
+
+	// !!! Is this really needed?
+	foreach (array('dh_server_public', 'enc_mac_key') as $key)
+		if (isset($assoc_data[$key]))
+			$assoc_data[$key] = str_replace(' ', '+', $assoc_data[$key]);
+
+	// Figure out the Diffie-Hellman secret.
+	if (!empty($assoc_data['enc_mac_key']))
+	{
+		$dh_secret = bcpowmod(binary_to_long(base64_decode($assoc_data['dh_server_public'])), $dh_keys['private'], $p);
+		$secret = base64_encode(binary_xor(sha1_raw(long_to_binary($dh_secret)), base64_decode($assoc_data['enc_mac_key'])));
+	}
+	else
+		$secret = $assoc_data['mac_key'];
+
+	// Store the data
+	$smcFunc['db_insert']('replace',
+		'{db_prefix}openid_assoc',
+		array('server_url' => 'string', 'handle' => 'string', 'secret' => 'string', 'issued' => 'int', 'expires' => 'int', 'assoc_type' => 'string'),
+		array($server, $handle, $secret, $issued, $expires, $assoc_type),
+		array('server_url', 'handle')
+	);
+
+	return array(
+		'server' => $server,
+		'handle' => $assoc_data['assoc_handle'],
+		'secret' => $secret,
+		'issued' => $issued,
+		'expires' => $expires,
+		'assoc_type' => $assoc_data['assoc_type'],
+	);
+}
+
+function smf_openID_removeAssociation($handle)
+{
+	global $smcFunc;
+
+	$smcFunc['db_query']('openid_remove_association', '
+		DELETE FROM {db_prefix}openid_assoc
+		WHERE handle = {string:handle}',
+		array(
+			'handle' => $handle,
+		)
+	);
+}
+
+function smf_openID_return()
+{
+	global $smcFunc, $user_info, $user_profile, $sourcedir, $modSettings, $context, $sc, $user_settings;
+
+	// Is OpenID even enabled?
+	if (empty($modSettings['enableOpenID']))
+		fatal_lang_error('no_access', false);
+
+	if (!isset($_GET['openid_mode']))
+		fatal_lang_error('openid_return_no_mode', false);
+
+	// !!! Check for error status!
+	if ($_GET['openid_mode'] != 'id_res')
+		fatal_lang_error('openid_not_resolved');
+
+	// SMF has this annoying habit of removing the + from the base64 encoding.  So lets put them back.
+	foreach (array('openid_assoc_handle', 'openid_invalidate_handle', 'openid_sig', 'sf') as $key)
+		if (isset($_GET[$key]))
+			$_GET[$key] = str_replace(' ', '+', $_GET[$key]);
+
+	// Did they tell us to remove any associations?
+	if (!empty($_GET['openid_invalidate_handle']))
+		smf_openid_removeAssociation($_GET['openid_invalidate_handle']);
+
+	$server_info = smf_openid_getServerInfo($_GET['openid_identity']);
+
+	// Get the association data.
+	$assoc = smf_openID_getAssociation($server_info['server'], $_GET['openid_assoc_handle'], true);
+	if ($assoc === null)
+		fatal_lang_error('openid_no_assoc');
+
+	$secret = base64_decode($assoc['secret']);
+
+	$signed = explode(',', $_GET['openid_signed']);
+	$verify_str = '';
+	foreach ($signed as $sign)
+	{
+		$verify_str .= $sign . ':' . strtr($_GET['openid_' . str_replace('.', '_', $sign)], array('&amp;' => '&')) . "\n";
+	}
+
+	$verify_str = base64_encode(sha1_hmac($verify_str, $secret));
+
+	if ($verify_str != $_GET['openid_sig'])
+	{
+		fatal_lang_error('openid_sig_invalid', 'critical');
+	}
+
+	if (!isset($_SESSION['openid']['saved_data'][$_GET['t']]))
+		fatal_lang_error('openid_load_data');
+
+	$openid_uri = $_SESSION['openid']['saved_data'][$_GET['t']]['openid_uri'];
+	$modSettings['cookieTime'] = $_SESSION['openid']['saved_data'][$_GET['t']]['cookieTime'];
+
+	if (empty($openid_uri))
+		fatal_lang_error('openid_load_data');
+
+	// Any save fields to restore?
+	$context['openid_save_fields'] = isset($_GET['sf']) ? unserialize(base64_decode($_GET['sf'])) : array();
+
+	// Is there a user with this OpenID_uri?
+	$result = $smcFunc['db_query']('', '
+		SELECT passwd, id_member, id_group, lngfile, is_activated, email_address, additional_groups, member_name, password_salt,
+			openid_uri
+		FROM {db_prefix}members
+		WHERE openid_uri = {string:openid_uri}',
+		array(
+			'openid_uri' => $openid_uri,
+		)
+	);
+
+	$member_found = $smcFunc['db_num_rows']($result);
+
+	if (!$member_found && isset($_GET['sa']) && $_GET['sa'] == 'change_uri' && !empty($_SESSION['new_openid_uri']) && $_SESSION['new_openid_uri'] == $openid_uri)
+	{
+		// Update the member.
+		updateMemberData($user_settings['id_member'], array('openid_uri' => $openid_uri));
+
+		unset($_SESSION['new_openid_uri']);
+		$_SESSION['openid'] = array(
+			'verified' => true,
+			'openid_uri' => $openid_uri,
+		);
+
+		// Send them back to profile.
+		redirectexit('action=profile;area=authentication;updated');
+	}
+	elseif (!$member_found)
+	{
+		// Store the received openid info for the user when returned to the registration page.
+		$_SESSION['openid'] = array(
+			'verified' => true,
+			'openid_uri' => $openid_uri,
+		);
+		if (isset($_GET['openid_sreg_nickname']))
+			$_SESSION['openid']['nickname'] = $_GET['openid_sreg_nickname'];
+		if (isset($_GET['openid_sreg_email']))
+			$_SESSION['openid']['email'] = $_GET['openid_sreg_email'];
+		if (isset($_GET['openid_sreg_dob']))
+			$_SESSION['openid']['dob'] = $_GET['openid_sreg_dob'];
+		if (isset($_GET['openid_sreg_gender']))
+			$_SESSION['openid']['gender'] = $_GET['openid_sreg_gender'];
+
+		// Were we just verifying the registration state?
+		if (isset($_GET['sa']) && $_GET['sa'] == 'register2')
+		{
+			require_once($sourcedir . '/Register.php');
+			return Register2(true);
+		}
+		else
+			redirectexit('action=register');
+	}
+	elseif (isset($_GET['sa']) && $_GET['sa'] == 'revalidate' && $user_settings['openid_uri'] == $openid_uri)
+	{
+		$_SESSION['openid_revalidate_time'] = time();
+
+		// Restore the get data.
+		require_once($sourcedir . '/Subs-Auth.php');
+		$_SESSION['openid']['saved_data'][$_GET['t']]['get']['openid_restore_post'] = $_GET['t'];
+		$query_string = construct_query_string($_SESSION['openid']['saved_data'][$_GET['t']]['get']);
+
+		redirectexit($query_string);
+	}
+	else
+	{
+		$user_settings = $smcFunc['db_fetch_assoc']($result);
+		$smcFunc['db_free_result']($result);
+
+		$user_settings['passwd'] = sha1(strtolower($user_settings['member_name']) . $secret);
+		$user_settings['password_salt'] = substr(md5(mt_rand()), 0, 4);
+
+		updateMemberData($user_settings['id_member'], array('passwd' => $user_settings['passwd'], 'password_salt' => $user_settings['password_salt']));
+
+		// Cleanup on Aisle 5.
+		$_SESSION['openid'] = array(
+			'verified' => true,
+			'openid_uri' => $openid_uri,
+		);
+
+		require_once($sourcedir . '/LogInOut.php');
+
+		if (!checkActivation())
+			return;
+
+		DoLogin();
+	}
+}
+
+function smf_openID_canonize($uri)
+{
+	// !!! Add in discovery.
+
+	if (strpos($uri, 'http://') !== 0 && strpos($uri, 'https://') !== 0)
+		$uri = 'http://' . $uri;
+
+	if (strpos(substr($uri, strpos($uri, '://') + 3), '/') === false)
+		$uri .= '/';
+
+	return $uri;
+}
+
+function smf_openid_member_exists($url)
+{
+	global $smcFunc;
+
+	$request = $smcFunc['db_query']('openid_member_exists', '
+		SELECT mem.id_member, mem.member_name
+		FROM {db_prefix}members AS mem
+		WHERE mem.openid_uri = {string:openid_uri}',
+		array(
+			'openid_uri' => $url,
+		)
+	);
+	$member = $smcFunc['db_fetch_assoc']($request);
+	$smcFunc['db_free_result']($request);
+
+	return $member;
+}
+
+// Prepare for a Diffie-Hellman key exchange.
+function smf_openID_setup_DH($regenerate = false)
+{
+	global $p, $g;
+
+	// First off, do we have BC Math available?
+	if (!function_exists('bcpow'))
+		return false;
+
+	// Defined in OpenID spec.
+	$p = '155172898181473697471232257763715539915724801966915404479707795314057629378541917580651227423698188993727816152646631438561595825688188889951272158842675419950341258706556549803580104870537681476726513255747040765857479291291572334510643245094715007229621094194349783925984760375594985848253359305585439638443';
+	$g = '2';
+
+	// Make sure the scale is set.
+	bcscale(0);
+
+	return smf_openID_get_keys($regenerate);
+}
+
+function smf_openID_get_keys($regenerate)
+{
+	global $modSettings, $p, $g;
+
+	// Ok lets take the easy way out, are their any keys already defined for us? They are changed in the daily maintenance scheduled task.
+	if (!empty($modSettings['dh_keys']) && !$regenerate)
+	{
+		// Sweeeet!
+		list ($public, $private) = explode("\n", $modSettings['dh_keys']);
+		return array(
+			'public' => base64_decode($public),
+			'private' => base64_decode($private),
+		);
+	}
+
+	// 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.
+	$private = smf_openid_generate_private_key();
+	$public = bcpowmod($g, $private, $p);
+
+	// Now that we did all that work, lets save it so we don't have to keep doing it.
+	$keys = array('dh_keys' => base64_encode($public) . "\n" . base64_encode($private));
+	updateSettings($keys);
+
+	return array(
+		'public' => $public,
+		'private' => $private,
+	);
+}
+
+function smf_openid_generate_private_key()
+{
+	global $p;
+	static $cache = array();
+
+	$byte_string = long_to_binary($p);
+
+	if (isset($cache[$byte_string]))
+		list ($dup, $num_bytes) = $cache[$byte_string];
+	else
+	{
+		$num_bytes = strlen($byte_string) - ($byte_string[0] == "\x00" ? 1 : 0);
+
+		$max_rand = bcpow(256, $num_bytes);
+
+		$dup = bcmod($max_rand, $num_bytes);
+
+		$cache[$byte_string] = array($dup, $num_bytes);
+	}
+
+	do
+	{
+		$str = '';
+		for ($i = 0; $i < $num_bytes; $i += 4)
+			$str .= pack('L', mt_rand());
+
+		$bytes = "\x00" . $str;
+
+		$num = binary_to_long($bytes);
+	} while (bccomp($num, $dup) < 0);
+
+	return bcadd(bcmod($num, $p), 1);
+}
+
+function smf_openID_getServerInfo($openid_url)
+{
+	global $sourcedir;
+
+	require_once($sourcedir . '/Subs-Package.php');
+
+	// Get the html and parse it for the openid variable which will tell us where to go.
+	$webdata = fetch_web_data($openid_url);
+
+	if (empty($webdata))
+		return false;
+
+	$response_data = array();
+
+	// Some OpenID servers have strange but still valid HTML which makes our job hard.
+	if (preg_match_all('~<link([\s\S]*?)/?>~i', $webdata, $link_matches) == 0)
+		fatal_lang_error('openid_server_bad_response');
+
+	foreach ($link_matches[1] as $link_match)
+	{
+		if (preg_match('~rel="([\s\S]*?)"~i', $link_match, $rel_match) == 0 || preg_match('~href="([\s\S]*?)"~i', $link_match, $href_match) == 0)
+			continue;
+
+		$rels = preg_split('~\s+~', $rel_match[1]);
+		foreach ($rels as $rel)
+			if (preg_match('~openid2?\.(server|delegate|provider)~i', $rel, $match) != 0)
+				$response_data[$match[1]] = $href_match[1];
+	}
+
+	if (empty($response_data['server']))
+		if (empty($response_data['provider']))
+			fatal_lang_error('openid_server_bad_response');
+		else
+			$response_data['server'] = $response_data['provider'];
+
+	return $response_data;
+}
+
+function sha1_hmac($data, $key)
+{
+
+	if (strlen($key) > 64)
+		$key = sha1_raw($key);
+
+	// Pad the key if need be.
+	$key = str_pad($key, 64, chr(0x00));
+	$ipad = str_repeat(chr(0x36), 64);
+	$opad = str_repeat(chr(0x5c), 64);
+	$hash1 = sha1_raw(($key ^ $ipad) . $data);
+	$hmac = sha1_raw(($key ^ $opad) . $hash1);
+	return $hmac;
+}
+
+function sha1_raw($text)
+{
+	if (version_compare(PHP_VERSION, '5.0.0') >= 0)
+		return sha1($text, true);
+
+	$hex = sha1($text);
+	$raw = '';
+	for ($i = 0; $i < 40; $i += 2)
+	{
+		$hexcode = substr($hex, $i, 2);
+		$charcode = (int) base_convert($hexcode, 16, 10);
+		$raw .= chr($charcode);
+	}
+
+	return $raw;
+}
+
+function binary_to_long($str)
+{
+	$bytes = array_merge(unpack('C*', $str));
+
+	$n = 0;
+
+	foreach ($bytes as $byte)
+	{
+		$n = bcmul($n, 256);
+		$n = bcadd($n, $byte);
+	}
+
+	return $n;
+}
+
+function long_to_binary($value)
+{
+	$cmp = bccomp($value, 0);
+	if ($cmp < 0)
+		fatal_error('Only non-negative integers allowed.');
+
+	if ($cmp == 0)
+		return "\x00";
+
+	$bytes = array();
+
+	while (bccomp($value, 0) > 0)
+	{
+		array_unshift($bytes, bcmod($value, 256));
+		$value = bcdiv($value, 256);
+	}
+
+	if ($bytes && ($bytes[0] > 127))
+		array_unshift($bytes, 0);
+
+	$return = '';
+	foreach ($bytes as $byte)
+		$return .= pack('C', $byte);
+
+	return $return;
+}
+
+function binary_xor($num1, $num2)
+{
+	$return = '';
+
+	for ($i = 0; $i < strlen($num2); $i++)
+		$return .= $num1[$i] ^ $num2[$i];
+
+	return $return;
+}
+
+// PHP 4 didn't have bcpowmod.
+if (!function_exists('bcpowmod') && function_exists('bcpow'))
+{
+	function bcpowmod($num1, $num2, $num3)
+	{
+		return bcmod(bcpow($num1, $num2), $num3);
+	}
+}
+
+?>
\ No newline at end of file