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

Xaraya + Twitter



Ингредиенты


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

Рецепт


Мы хотим обеспечить следующее поведение:
  • если пользователь не за логином — показать кнопку «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>

Повторюсь, теперь мы должны увидеть что-то вот такое:
mudasobwa: logged in

Еще рецепты