Pull to refresh

SteamID авторизация для MODX Revo

Здравствуйте, хабровчане. Недавно у меня возникла необходимость сделать для сайта на любимом модиксе аутентификацию через Steam. Гугление, хабрение и даже стековерфлоувание (какое страшное слово получилось) готового результата не принесло, так что решил я собранные в разных местах куски объединить в одну статью, чтобы тем, кто пойдёт следом было легче.

Steam-аутентификация


Первым делом, как и большинство умных людей (по крайней мере тех, чьи посты я позже находил на разных форумах с вопросами «АААА КАК ЭТО РАБОТАЕТ???!!!»), направился я на http://steamcommunity.com/dev, где и прочитал весьма воодушевляющий абзац:

Steam can act as an OpenID provider. This allows your application to authenticate a user's SteamID without requiring them to enter their Steam username or password on your site (which would be a violation of the API Terms of Use.) Just download an OpenID library for your language and platform of choice and use steamcommunity.com/openid as the provider. The returned Claimed ID will contain the user's 64-bit SteamID. The Claimed ID format is: steamcommunity.com/openid/id/steamid


Скачав simpleOpenID и вдоволь с ним наигравшись, я обнаружил, что стим в качестве OpenID-провайдера им вообще никаким образом не рассматривается. Ещё немного поисков, и вот удача. На одном из форумов обнаружился класс, разработанный исключительно для авторизации через стим.

Правда, для использования в модиксе, в него пришлось внести некоторые доработки.

Файлик с именем steamsignin.class.php
<?php
/**
*
* @package Steam Community API
* @copyright (c) 2010 ichimonai.com
* @license http://opensource.org/licenses/mit-license.php The MIT License
*
*/

class SteamSignIn
{
	const STEAM_LOGIN = 'https://steamcommunity.com/openid/login';

	/**
	* Get the URL to sign into steam
	*
	* @param mixed returnTo URI to tell steam where to return, MUST BE THE FULL URI WITH THE PROTOCOL
	* @param bool useAmp Use & in the URL, true; or just &, false. 
	* @return string The string to go in the URL
	*/
	public static function genUrl($returnTo = false, $useAmp = true)
	{
		//$returnTo = (!$returnTo) ? (!empty($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'] : $returnTo;
		//Эту строчку правим на
		$returnTo = (!$returnTo) ? $_SERVER['SCRIPT_NAME'] : $returnTo;
		$returnTo = (!empty($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'].'/'. $returnTo;
		/*поскольку вызывать будем из модикса, где $_SERVER['SCRIPT_NAME'] не указывает на страницу, с которой был вызов. А отправлять пользователя на главную страницу сайта некультурно*/

		$params = array(
			'openid.ns'			=> 'http://specs.openid.net/auth/2.0',
			'openid.mode'		=> 'checkid_setup',
			'openid.return_to'	=> $returnTo,
			'openid.realm'		=> (!empty($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'],
			'openid.identity'	=> 'http://specs.openid.net/auth/2.0/identifier_select',
			'openid.claimed_id'	=> 'http://specs.openid.net/auth/2.0/identifier_select',
		);
		
		$sep = ($useAmp) ? '&' : '&';
		return self::STEAM_LOGIN . '?' . http_build_query($params, '', $sep);
	}
	
	/**
	* Validate the incoming data
	*
	* @return string Returns the SteamID64 if successful or empty string on failure
	*/
	public static function validate()
	{
		// Star off with some basic params
		$params = array(
			'openid.assoc_handle'	=> $_GET['openid_assoc_handle'],
			'openid.signed'			=> $_GET['openid_signed'],
			'openid.sig'			=> $_GET['openid_sig'],
			'openid.ns'				=> 'http://specs.openid.net/auth/2.0',
		);
		
		// Get all the params that were sent back and resend them for validation
		$signed = explode(',', $_GET['openid_signed']);
		foreach($signed as $item)
		{
			$val = $_GET['openid_' . str_replace('.', '_', $item)];
			$params['openid.' . $item] = get_magic_quotes_gpc() ? stripslashes($val) : $val; 
		}

		// Finally, add the all important mode. 
		$params['openid.mode'] = 'check_authentication';
		
		// Stored to send a Content-Length header
		$data =  http_build_query($params);
		$context = stream_context_create(array(
			'http' => array(
				'method'  => 'POST',
				'header'  => 
					"Accept-language: en\r\n".
					"Content-type: application/x-www-form-urlencoded\r\n" .
					"Content-Length: " . strlen($data) . "\r\n",
				'content' => $data,
			),
		));

		//Работоспособность этой функции с https зависит от настроек хостинга и у меня по закону Мэрфи не сработала.
		//$result = file_get_contents(self::STEAM_LOGIN, false, $context);
		//так что, пришлось заменять её на 
		$ch = curl_init();
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
		curl_setopt($ch, CURLOPT_HEADER, false);
		curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
		curl_setopt($ch, CURLOPT_URL, self::STEAM_LOGIN.'?'.$data);
		curl_setopt($ch, CURLOPT_REFERER, self::STEAM_LOGIN.'?'.$data);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
		$result = curl_exec($ch);
		curl_close($ch);
		
		// Validate wheather it's true and if we have a good ID
		preg_match("#^http://steamcommunity.com/openid/id/([0-9]{17,25})#", $_GET['openid_claimed_id'], $matches);
		$steamID64 = is_numeric($matches[1]) ? $matches[1] : 0;

		// Return our final value
		//return preg_match("#is_valid\s*:\s*true#i", $result) == 1 ? $steamID64 : '';
		//Поскольку одной валидации нам мало, то добавляем мякотку в виде ника и аватарки (при желании, можно читать и другие элементы)
		$slf = "http://steamcommunity.com/profiles/$steamID64/?xml=1";
		 $url = simplexml_load_file($slf);
        
		$ret = array("result"=>true,
		 "nick"=>(string)$url->steamID[0],
		"username"=>(string)$url->steamID64[0],
		"avatarIcon"=>(string)$url->avatarIcon[0],
		 "avatarMedium"=>(string)$url->avatarMedium[0],
		"avatarFull"=>(string)$url->avatarFull[0]);
		// Return our final value
		return preg_match("#is_valid\s*:\s*true#i", $result) == 1 ? $ret : array("result"=>false);

	}
}



И вот, собрав такой класс и положив в папку, которую смогу запомнить, приступил ко второй части.

Что же происходит в модиксе



С модиксом вопросов возникло меньше, он мне знаком чуть поболе, нежели стим, учётку для которого я использовал впервые именно для тестирования авторизации.

Итак, создаём сниппет со следующим кодом:
Сниппет с говорящим названием SteamOpenID
<?php
if(!isset($scriptProperties['tmpl_loggedin'])){ $tmpl_loggedin = "OpenIDLoggedIn"; }
if(!isset($scriptProperties['tmpl_loggedout'])){ $tmpl_loggedout = "OpenIDNotLoggedIn"; } 

//Первые две строки вряд ли вызовут вопросы, да и следующая весьма обыденна. Подключаем класс к сниппету.
$modx->getService('steamsignin','SteamSignIn', $modx->getOption('base_path').'path/to/class');

$errors = "";

/*Итак, если мы выходим, то просто разлогиниваем пользовтеля и перезагружаем страницу. Перезагружаем, потому что метод  $user = $modx->getObject('modUser', 0), предложенный на каком-то из форумов широко известным в узких кругах Безумкиным выдавал ошибки. */
if($_GET['openid_action'] == "logout"){
    $response = $modx->runProcessor('/security/logout');
    if ($response->isError()) {
    // ошибка
    $errors.='Logout error. Message: '.$response->getMessage();
    }
    $url = $modx->makeUrl($modx->resource->id);
    $modx->sendRedirect($url);
}

//Если мы не выходим, то возможно логинимся
else if($_GET['openid_mode'] == 'id_res'){ 	// Perform HTTP Request to OpenID server to validate key
    
    $sign = new SteamSignIn;
    $result = $sign->validate();
    //а если мы логинимся и стим отзывается полжительно, то в $result лежат всякие интересные вещи. Например, true в $result['result']
    if($result['result']){
        
        $count = $modx->getCount('modUser', array('username' => $result['username']));
        
        //здесь мы создаём в модиксе пользователя c SteamID64 в качестве логина. Потому что ники менять можно, а айдишники нельзя
        if($count == 0){
            $user = $modx->newObject('modUser');
            $user->set('username', $result['username']);
            $user->set('password', $modx->runProcessor('path/to/processor/createRandomPass')->getMessage());
            $user->save();
            $profile = $modx->newObject('modUserProfile');
            $profile->set('fullname', $result['nick']);
            $extended['avatar']['icon']=$result['avatarIcon'];
            $extended['avatar']['medium']=$result['avatarMedium'];
            $extended['avatar']['full']=$result['avatarFull'];
            $profile->set('extended', $extended);
            $user->addOne($profile);
            $profile->save();
            $user->save();
            }
        
        /*был ли уже пользователь, или мы только что его создали - сбрасываем пароль на случайный и входим под этим паролем. Чтоб не заморачиваться с их хранением*/
        $pass = $modx->runProcessor('path/to/processor/createRandomPass');
        $user = $modx->getObject('modUser', array('username' => $result['username']));
        $user->set('password', $pass->getMessage());
        $user->save();
        
        $logindata = array(
            'username' => $result['username'], // имя пользователя
            'password' => $pass->getMessage(), // пароль
            'rememberme' => true // запомнить?
        );
        // сам процесс авторизации
        $response = $modx->runProcessor('/security/login', $logindata);
        // проверяем, успешно ли
        if ($response->isError()) {
        // произошла ошибка, например неверный пароль
            $errors.='Login error. Message: '.$response->getMessage();
        }
        $modx->user = $user; //А вот эта конструкция срабатывает, перезагрузка страницы не нужна.
}}


//Дальше всё совсем просто. Определяем, авторизован ли пользователь и загружаем соответствующие чанки
//Плейсхолдеры в чанках простые, при необходимости опять же можно расширить их состав.
if($modx->user->id!=0) {
    $profile = $modx->user->getOne('Profile');
    $ext = $profile->get('extended');
	$output = $modx->getChunk($tmpl_loggedin,array('identity'=>$profile->fullname,'avatar'=>$ext['avatar']['icon'],'errors'=>$errors));
	return $output;
} else {
        $sign = new SteamSignIn;
    	$output = $modx->getChunk($tmpl_loggedout,array('url'=>$sign->genUrl($modx->resource->get('uri')),'errors'=>$errors));
    	return $output;
}



Вместо заключения


В коде сниппета использовался самописный процессор для генерации пароля. Конечно, соблазн просто написать функцию и не заморачиваться, всегда выше. Но статья Парадигма программирования процессорами в MODx Revolution постоянно маячила перед глазами и сподвигала на правильные решения.
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.