Прошли те времена, когда каждый форум на персональной страничке каждого Васисуалия Свердыщенко требовал отдельной регистрации.
Мы потихонечку привыкаем к тому, что оставить комментарий от имени своего 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>
Повторюсь, теперь мы должны увидеть что-то вот такое:

