Pull to refresh

Сквозная авторизация на своем сайте через Twitter

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

Еще рецепты


Tags:
Hubs:
+48
Comments30

Articles