Прошли те времена, когда каждый форум на персональной страничке каждого Васисуалия Свердыщенко требовал отдельной регистрации.
Мы потихонечку привыкаем к тому, что оставить комментарий от имени своего OpenID/OAuth провайдера можно фактически везде. Также для всех популярных CMS давно написаны плагины сквозной авторизации Twitter/Facebook/Google/Яndex/Вконтакте. Кроме того есть DISQUS… Но что делать, если мы хотим предоставить пользователю стороннего сервиса какие-то дополнительные полномочия, не вынуждая его заводить отдельную учетную запись на нашем сайте? Особенно, если для нашей CMS пока нет чудо-плагина?
Я расскажу о том, как быстро и безболезненно прикрутить сквозную авторизацию к экзотической CMS и какие на этом пути встречаются подводные грабли.

Для примера я выберу авторизацию через Twitter в CMS Xaraya. Выбор провайдера обусловлен его популярностью. CMS именно эта — по трем причинам: она мне нравится, она малоизвестна (и, как следствие, мало отличается от самописной) и, наконец, она грамотно спроектирована (при чем тут это — станет ясно немного позже).
Архитектура MVC Xaraya такова, что все элементы каждой страницы зависят от контекста. Каждый модуль может уметь (а может — и не уметь) выводить как основное содержимое (например: ленту записей блога, отдельные записи блога), так и т. н. «блоки», встраиваемые обычно в боковые колонки и показываемые рядом с любым иным содержимым, сгенерированным другими модулями. Впрочем, что я тут распинаюсь — так устроены почти все CMS. Оговорюсь только, что в коде ниже остались некоторые специфичные для Xaraya функции — они все начинаются с префикса «
Наш модуль не будет выводить никакого «основного» содержимого. Он будет состоять из одного блока, в котором будет либо кнопочка (пользователь не за логином):

либо визитная карточка пользователя за логином:

Мы хотим обеспечить следующее поведение:
Основная проблема заключается в том, что OAuth подразумевает двойное перенаправление на сторонний сайт (сайт провайдера авторизации, в нашем случае — twitter.com). Поэтому из блока нашей CMS его просто так не выполнить — после походов за токенами и авторизацией — потеряется контекст текущей сессии. Стало быть, придется извращаться с выскакивающими окнами (знаю, знаю, что некрасиво, но что поделать…).
Моя CMS умеет показывать разные шаблоны блока для простых странников и пользователей за логином.
Начнем с самого простого — сделаем шаблон блока для странников. В нем будет одна лишь кнопка, и немного скрипта:
Функция
Twitter хранит токены авторизации для каждого приложения (а наша CMS для них — приложение) вечно, поэтому если пользователь хоть раз регистрировался на нашем сайте — цепочка переходов не потребует пользовательского ввода. Если это первая попытка логина к нам — Twitter спросит стандартное Allow/Deny. А если пользователь не залогинен в Twitter — его сначала попросят залогиниться. После всего этого, если пользователь нажал «Allow» — мы получим обратный редирект по адресу, который мы передали в
Теперь у нас есть сокровище — данные пользователя, которые отдал нам Twitter. Пора вернуть их в наше приложение:
Ага. Теперь пора повозиться с собственной системой авторизации.
Итак, наш блок получил данные о пользователе. Перво-наперво — закроем надоевшее всплывающее окно. Затем — заполним псевдо-форму (а точнее, скрытую форму) полученными данными и пойдем проверять, знаком ли нам этот пользователь, или его еще придется регистрировать в нашей базе:
Все эти данные нам потребуются для отрисовки визитки пользователя его любимыми цветами. Форма, как вы понимаете, была подготовлена заранее обычным HTML. Вот куда пойдет
Теперь нам осталось только подготовиться к тому, что CMS захочет сменить шаблон нашего блока на другой вариант: для пользователя за логином. Я поленился оформлять этот код по-человечески, все равно всю логику мы уже отработали. Судите строго :-)
Повторюсь, теперь мы должны увидеть что-то вот такое:

Мы потихонечку привыкаем к тому, что оставить комментарий от имени своего OpenID/OAuth провайдера можно фактически везде. Также для всех популярных CMS давно написаны плагины сквозной авторизации Twitter/Facebook/Google/Яndex/Вконтакте. Кроме того есть DISQUS… Но что делать, если мы хотим предоставить пользователю стороннего сервиса какие-то дополнительные полномочия, не вынуждая его заводить отдельную учетную запись на нашем сайте? Особенно, если для нашей CMS пока нет чудо-плагина?
Я расскажу о том, как быстро и безболезненно прикрутить сквозную авторизацию к экзотической CMS и какие на этом пути встречаются подводные грабли.

Ингредиенты
Для примера я выберу авторизацию через Twitter в CMS Xaraya. Выбор провайдера обусловлен его популярностью. CMS именно эта — по трем причинам: она мне нравится, она малоизвестна (и, как следствие, мало отличается от самописной) и, наконец, она грамотно спроектирована (при чем тут это — станет ясно немного позже).
Архитектура MVC Xaraya такова, что все элементы каждой страницы зависят от контекста. Каждый модуль может уметь (а может — и не уметь) выводить как основное содержимое (например: ленту записей блога, отдельные записи блога), так и т. н. «блоки», встраиваемые обычно в боковые колонки и показываемые рядом с любым иным содержимым, сгенерированным другими модулями. Впрочем, что я тут распинаюсь — так устроены почти все CMS. Оговорюсь только, что в коде ниже остались некоторые специфичные для Xaraya функции — они все начинаются с префикса «
xar
» и по названию всегда понятно, что они делают. На понимание механизма это не должно никак повлиять.Наш модуль не будет выводить никакого «основного» содержимого. Он будет состоять из одного блока, в котором будет либо кнопочка (пользователь не за логином):

либо визитная карточка пользователя за логином:

Рецепт
Мы хотим обеспечить следующее поведение:
- если пользователь не за логином — показать кнопку «Login with Twitter»;
- если мы пользователя узнали — показать его визитную карточку;
- если пользователь хочет залогиниться впервые — прозрачно создать «суррогатную» учетную запись в нашей базе и отдать управление состоянием нашей CMS;
- если же это повторный логин (сессия протухла, например, или он сам нажал «Выйти») — обновить пользовательские данные из его Twitter-account'а в нашей базе и залогинить его.
Основная проблема заключается в том, что OAuth подразумевает двойное перенаправление на сторонний сайт (сайт провайдера авторизации, в нашем случае — twitter.com). Поэтому из блока нашей CMS его просто так не выполнить — после походов за токенами и авторизацией — потеряется контекст текущей сессии. Стало быть, придется извращаться с выскакивающими окнами (знаю, знаю, что некрасиво, но что поделать…).
Способ приготовления
Моя CMS умеет показывать разные шаблоны блока для простых странников и пользователей за логином.
Начнем с самого простого — сделаем шаблон блока для странников. В нем будет одна лишь кнопка, и немного скрипта:
<img src="/modules/authtwitter/xarimages/darker.png" alt="Sign in with Twitter"
style="cursor:pointer;" onclick="popUp('#$url#');" id="loginBtn"/>
Функция
popUp
, как ни странно, откроет всплывающее окно, в котором мы и будем идти тернистым путем авторизации. Код собственно авторизации я позаимствовал у Abraham Williams, он есть на Twitter API Wiki. Наш первый уровень выглядит так: require_once(dirname(__FILE__) . '/../libs/twitteroauth/twitteroauth.php');
require_once(dirname(__FILE__) . '/../libs/twitteroauth/config.php');
/* Build TwitterOAuth object with client credentials. */
$connection = new TwitterOAuth(CONSUMER_KEY, CONSUMER_SECRET);
/* Get temporary credentials. */
$request_token = $connection->getRequestToken(OAUTH_CALLBACK);
/* Save temporary credentials to session. */
/* NB! The code below is specific to Xaraya! */
xarSessionSetVar('oauth_token', $request_token['oauth_token']);
xarSessionSetVar('oauth_token_secret', $request_token['oauth_token_secret']);
/* NB! End of specific to Xaraya codepiece. */
/* If last connection failed don't display authorization link. */
switch ($connection->http_code) {
case 200:
/* Build authorize URL and redirect user to Twitter. */
$url = $connection->getAuthorizeURL($request_token);
header('Location: ' . $url); // ⇐ That's why we needed a popup window
break;
default:
/* Immediately return if something went wrong. */
return USER_AUTH_FAILED;
}
Twitter хранит токены авторизации для каждого приложения (а наша CMS для них — приложение) вечно, поэтому если пользователь хоть раз регистрировался на нашем сайте — цепочка переходов не потребует пользовательского ввода. Если это первая попытка логина к нам — Twitter спросит стандартное Allow/Deny. А если пользователь не залогинен в Twitter — его сначала попросят залогиниться. После всего этого, если пользователь нажал «Allow» — мы получим обратный редирект по адресу, который мы передали в
getRequestToken
. Вот как нужно его обработать: require_once(dirname(__FILE__) . '/../libs/twitteroauth/twitteroauth.php');
require_once(dirname(__FILE__) . '/../libs/twitteroauth/config.php');
/* If the oauth_token is old—redirect to the connect page. */
if (
isset($_REQUEST['oauth_token']) &&
xarSessionGetVar('oauth_token') !== $_REQUEST['oauth_token']
) {
xarSessionSetVar('oauth_status', 'oldtoken');
xarRedirectUrl('…');
}
/* Create TwitteroAuth object with app key/secret and token key/secret from default phase */
$connection = new TwitterOAuth(
CONSUMER_KEY,
CONSUMER_SECRET,
xarSessionGetVar('oauth_token'),
xarSessionGetVar('oauth_token_secret')
);
/* Request access tokens from twitter */
$access_token = $connection->getAccessToken($_REQUEST['oauth_verifier']);
/* If HTTP response is 200 continue otherwise send to connect page to retry */
switch ($connection->http_code) {
case 200:
// http://apiwiki.twitter.com/w/page/22554689/Twitter-REST-API-Method%3A-account%C2%A0verify_credentials
$content = $connection->get('account/verify_credentials');
default:
xarRedirectUrl('…');
}
Теперь у нас есть сокровище — данные пользователя, которые отдал нам Twitter. Пора вернуть их в наше приложение:
if(window.opener != null && !window.opener.closed) {
window.opener.setCredentials(<?php echo json_encode($content); ?>);
}
Ага. Теперь пора повозиться с собственной системой авторизации.
Воробушек, иди к нам
Итак, наш блок получил данные о пользователе. Перво-наперво — закроем надоевшее всплывающее окно. Затем — заполним псевдо-форму (а точнее, скрытую форму) полученными данными и пойдем проверять, знаком ли нам этот пользователь, или его еще придется регистрировать в нашей базе:
function setCredentials(content) {
if (popUpObj) {
popUpObj.close();
popUpObj = null;
}
if (content && content.screen_name) {
document.getElementById("name").value = content.name;
document.getElementById("screenname").value = content.screen_name;
document.getElementById("profileimageurl").value = content.profile_image_url;
document.getElementById("url").value = content.url;
document.getElementById("statustext").value = content.status.text;
document.getElementById("description").value = content.description;
document.getElementById("profiletextcolor").value = content.profile_text_color;
document.getElementById("profilelinkcolor").value = content.profile_link_color;
document.getElementById("profilebordercolor").value = content.profile_sidebar_border_color;
document.getElementById("doAuthForm").submit();
}
}
Все эти данные нам потребуются для отрисовки визитки пользователя его любимыми цветами. Форма, как вы понимаете, была подготовлена заранее обычным HTML. Вот куда пойдет
submit
: extract($args);
$user_info = array(
'pass' => $pass,
'screenname' => $screenname,
'name' => $name,
'statustext' => $statustext,
'profileimageurl' => $profileimageurl,
'url' => $url,
'description' => $description,
'profiletextcolor' => $profiletextcolor,
'profilelinkcolor' => $profilelinkcolor,
'profilebordercolor' => $profilebordercolor
);
// Check, if the user already exists in our database
$userRole = xarGetRole(array('uname' => $user_info['screenname']));
if (!$userRole) {
$userRole = xarCreateRole(
array(
'uname' => $user_info['screenname'],
'realname' => $user_info['name'],
'email' => '', // Bloody Twitter does not provide emails
'pass' => $user_info['pass'],
'date' => time(),
'authmodule' => 'authtwitter'
)
);
}
/* Now we are to store user credentials so that when CMS will
* proceed with user registration and switch block to
* the template for logged in user, we could draw the card */
xarSessionSetVar('user_info', $user_info);
Ждем гостей
Теперь нам осталось только подготовиться к тому, что CMS захочет сменить шаблон нашего блока на другой вариант: для пользователя за логином. Я поленился оформлять этот код по-человечески, все равно всю логику мы уже отработали. Судите строго :-)
<div id="twCredentials" class="twcredentials">
<img id="twPhoto" class="twphoto" src="#$user_info['profileimageurl']#"/>
<span class="twlogout cuprum">
<a href="&xar-modurl-authsystem-user-logout;">
<xar:mlstring>Logout</xar:mlstring>
</a>
</span>
<img id="twServiceLogo" class="twservicelogo" src="/i/twitbird.png" />
<span id="twScreenName" class="twscreenname cuprum">
<a href="http://twitter.com/#$user_info['screenname']#">
#$user_info['screenname']#
</a>
</span>
<br/>
<span id="twName" class="twname ubuntu">
<xar:if condition="empty($user_info['url'])">
#$user_info['name']#
<xar:else />
<a href="#$user_info['url']#">#$user_info['name']#</a>
</xar:if>
</span>
<div class="twdescription ubuntu">
<span id="twDescription">
<xar:if condition="empty($user_info['statustext'])">
#$user_info['description']#
<xar:else />
#$user_info['statustext']#
</xar:if>
</span>
</div>
</div>
Повторюсь, теперь мы должны увидеть что-то вот такое:
