OpenID. Как я внедрял поддержку OpenID. Хроники

January 17, 2008

Продолжаю обживаться на новом хостинге. Пришло время реализовать еще несколько идей. Идей, реализация которых когда-то давно столкнулась с рядом технических трудностей, моей природной ленью, и извечным вопросом "кому это надо?". Столкнулась, не выдержала и заглохла. Так что, пришло время реанимировать пару старых задумок. Первая из этих идей - добавление openid-аутентификации для этого сайта. Сейчас он состоит из всего двух компонент: собственно, учебные материалы, статьи, заметки (построены на базе mediawiki) и обучающая машина mysql. В планах ближайшей пятилетки добавить еще несколько сервисов для сайта и, естественно, встал вопрос регистрации пользователей. Кто-то скажет, что никакая регистрация не нужна: в наше время высоких технологий и не менее высоких скоростей "поиска, чтения и забывания найденной в internet информации" никто не будет регистрироваться на каком-то там "жалком блоге", чтобы сделать пару комментариев. К тому же придумывать новые имена и пароли для сайтов-однодневок довольно напрягающее, само по себе занятие, а использовать везде одни и те же имена и пароли - занятие, скажу прямо, очень опасное.

А количество раз, когда я сам лично проходил регистрацию, требующую подтверждения по email можно пересчитать по пальцам одной руки: мне не жалко светить свой ящик для "спам-почты" - просто лень.

Положение немного спасают всяческие браузерные примочки в стиле "сохранить введенный пароль? Да. Нет." Немного помогают веб-сайты хранящие расшаренные имена и пароли для всеобщего, так сказать, использования. С другой стороны, есть люди, которые достаточно основательно подходят к вопросу чтения и комментирования и хотят представляться и не хотят, чтобы их именем пользовались другие. Особую категорию составляют "гербалайщики от internet", которые ходят по блогам, форумам и оставляют дурацкие комментарии с ссылками на их "откуда это рекламное г-но появилось здесь?!" сайты.

К тому же тут такое дело : У меня в обучающей машине есть не просто список пользователей, которые туда заходят и что-то решают, но и их рабочий кабинет. Где ведется учет, какие задачи решались, сколько времени на это было потрачено, какие были варианты ответов, так чтобы можно было посмотреть "как же я делал эту штуку тогда?".Так что обойтись только гостевыми аккаунтами не получится. Нужна регистрация, и сделать ее нужно в нескольких вариантах: регистрация, собственно, средствами сервера и регистрация через openid. Честно говоря, я никогда раньше не работал с openid, так что этот материал будет чем-то вроде "заметки по ходу изучения новой для меня темы".

Openid - это свободная, бесплатная и открытая инфраструктура позволяющая выполнять перенос учетных записей с одного сервера на другой. Многие из пользователей сети зарегистрированы на каком-либо из сервисов, вроде livejournal, и всяких там социальных сетей, ввели сведения о себе и почему бы не воспользоваться этой информацией для входа на другие сайты. Предположим, что вы зарегистрировались на живом журнале и вам выдали персональную страничку вида http : //vasyano.livejournal.com/. Поздравляю, теперь у вас есть уникальное сетевое имя и почему бы его не использовать как логин для регистрации на некотором сайте-блоге? Ага, размечтались. Отношения между openid провайдером (сайтом живого журнала) и сайтом потребителем этих услуг не очень просты. Ведь, враги повсюду, и они только и ждут момента, чтобы похитить вашу секретную информацию. Поэтому на форме отправки комментария нет поля для ввода пароля - только имя пользователя (точнее его openid-идентификатор). Наш Вася старательно ввел адрес своей страницы в поле логина. Сайт берет это имя (http : //vasyano.livejournal.com/), загружает страницу по указанному адресу и находит в ее html-коде следующие сакральные буквы:
  1. <link rel="openid.server" href="http : //www.livejournal.com/openid/server.bml" />
  2. <meta http-equiv="X-XRDS-Location" content="http : //users.livejournal.com/vasyano/data/yadis" />
Первая строка содержит адрес сервера openid аутентификации. Именно по этому адресу обратится сайт (точнее, какая-то openid библиотека) для того, чтобы проверить "А Вася тут живет?". Сайт обращается к openid-провайдеру, сообщая ему две вещи: имя пользователя и адрес страницы возврата. Клиент переходит на сайт провайдера openid-услуг, видит там форму ввода пароля (так злоумышленник не может представиться другим лицом) и там же у пользователя спрашивают "Вася, а ты веришь сайту мой-блог.ru и хочешь предоставить ему сведения о себе?". Вася отвечает, что мол, хочет. Затем провайдер openid перенаправляет браузер обратно на сайт блога (на страницу возврата), в этой сообщая каким-то образом, что да, действительно, "Вася тут зарегистрирован". Затем сайто-блог выполняет какие-то хитрые действия направленные на проверку того, что подтверждение было получено именно от того самого openid-провайдера к которому шло изначальное обращение и если это так, то аутентификация клиента считается успешной.

Где достать openid-провайдер?



Я выше говорил, что такие услуги предоставляют известные социальные сети (типа живого журнала, яндекса, одноклассников и прочая :). К сожалению, здесь возникает проблема в том, что таких сервисов очень много и не факт, что мне не надоест через какое-то время тот же живой журнал, и я не удалю ко всем чертям его аккаунт. Или сервис может рухнуть, разориться (ох, чую я скорый взрыва второго пузыря доткомов) и прочее и прочее. Более надежным для (продвинутых гиков) будет создание собственного провайдера open-id. Здесь вовсе не обязательно покупать дорогой хостинг и пол месяца устанавливать и настраивать какой-то софт. В составе openid есть понятие делегации. Идея в том, что вы на вашем сайто-блоге в код html-страницы (назовем ее "foo.html") помещаете следующий код:
  1. <link rel="openid.server" href=" http : //www.livejournal.com/openid/server.bml" />
  2. <link rel="openid.delegate" href="http : //vasyano.livejournal.com" />
Теперь, везде, где требуется openid-аутентификация, вы вводите адрес этой страницы, например, http : // мой-блог.ru/foo.html.

Когда дефолт накроет страну, поглотит при этом 90% социальных сетей и "бомжеблогов", вам будет не до Интернета (надо идти собирать макулатуру) и вы никогда не узнаете, что я вас злобно обманул. А если серьезно, если что-то случится с провайдером услуг живого журнала, то вы просто измените html-код страницы foo.html и укажите там другие значения для свойств "openid.server" и "openid.delegate". Естественно, что теперь вы должны пуще ока беречь хостинг, на котором размещен ваш сайт (http : // мой-блог.ru). Стоит ли этим заниматься решать вам.

Я же далее покажу, как установить openid-провайдер на собственном хостинге.

Прежде всего, необходимо скачать написанную на php библиотеку с сайта http://www.openidenabled.com/. Затем я распаковал архив в папку openid корня сервера и открыл страницу http : //center:89/openid/examples/server/server.php (справедливо решив, что начать нужно с идущего вместе с библиотекой примера openid-сервера). В ответ я получил сообщение, что для работы openid-сервера нужен какой-то конфиг и я его должен написать. Ладно, в папке openid/examples/server лежит файл setup.php, если его открыть в браузере, то появится простенькая html-форма, где нужно указать настройки сервера.



Из заслуживающего внимания там только опция, где нужно указать путь к каталогу, хранящему библиотеку openid (я ее оставил пустой так, чтобы заполнить в последствии ручками) и то, где будут храниться данные (в файловой системе или в базе данных). База данных может быть как mysql так и sqlite (краткое введение в sqlite будет в планируемой на днях публикации статьи про google gears). Из-за соображений централизации я отбросил вариант файловой системы и sqlite: мне проще сделать один бэкап базы, чем заниматься копированием десятка файликов разбросанных "черт знает где".

В получившемся файле
  1. <?php
  2. /**
  3.  * The URL for the server.
  4.  *
  5.  * This is the location of server.php. For example:
  6.  *
  7.  * $server_url = 'http://example.com/~user/server.php';
  8.  *
  9.  * This must be a full URL.
  10.  */
  11. $server_url = "http://study-and-dev.com/openid/examples/server/server.php";
  12.  
  13. set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/../../openid/');
  14.  
  15. /**
  16.  * Initialize an OpenID store
  17.  *
  18.  * @return object $store an instance of OpenID store (see the
  19.  * documentation for how to create one)
  20.  */
  21. function getOpenIDStore()
  22. {
  23.     require_once 'Auth/OpenID/MySQLStore.php';
  24.     require_once 'DB.php';
  25.  
  26.     $dsn = array(
  27.                  'phptype'  => 'mysql',
  28.                  'username' => 'root',
  29.                  'password' => '123',
  30.                  'hostspec' => 'center'
  31.                  );
  32.     $db =& DB::connect($dsn);
  33.  
  34.     if (PEAR::isError($db)) {
  35.         return null;
  36.     }
  37.  
  38.     $db->query("USE openidbase");
  39.     $s =& new Auth_OpenID_MySQLStore($db);
  40.     $s->createTables();
  41.     return $s;
  42. }
  43. ?>
Я добавил строчку, подключающую к переменной include_path (где выполнять поиск затребованных для подключения библиотек) путь к корню установки openid (использовал относительный путь). Затем сохранил файл под именем config.php в каталоге вместе с server.php.

Затем открываю http : //center:89/openid/examples/server/server.php и любуюсь на свежеустановленный и даже рабочий openid-сервер (по крайней мере, когда я нажал на кнопку "login", ввел придуманное имя пользователя, то ничего не "поломалось", а вверху страницы отобразилась надпись, вроде, "добро пожаловать, Вася").





Открыл посмотреть исходный код html для персональной страницы. Как и ожидалось там нашлась директирива:
  1. <html>
  2. <head>
  3.   <link rel="openid2.provider openid.server" href="http://study-and-dev.com:89/openid/examples/server/server.php/userXrds?user=vasyano"/>
  4.   <meta http-equiv="X-XRDS-Location" content="http://study-and-dev.com:89/openid/examples/server/server.php" />
  5. </head>
  6. <body>
  7.   This is the identity page for users of this server.
  8. </body>
  9. </html>
Очень смутило наличие двух двоеточий в адресе страницы. Дело в том, что все свои проекты я запускаю под нестандартным портом (89). У меня установлен сервис Microsoft IIS, который как раз таки работает под 80-кой, а для apache я отвел порт № 89. Путем несложных поисков я определил место, где формируется столь странный адрес. Это был файл openid\examples\server\lib\session.php". Так что я подправил код функции getServerURL следующим образом:
  1. /**
  2.  * Get the URL of the current script
  3.  */
  4. function getServerURL()
  5. {
  6.     $path = $_SERVER['SCRIPT_NAME'];
  7.     $host = $_SERVER['HTTP_HOST'];
  8.     $port = $_SERVER['SERVER_PORT'];
  9.  
  10.     $arr_p2 = explode (':', $host);
  11.     if (is_array($arr_p2) && count($arr_p2) == 2){
  12.       $host = $arr_p2[0];
  13.       $port = $arr_p2[1];
  14.     }
  15.     $s = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] ? 's' : '';
  16.     if (($s && $port == "443") || (!$s && $port == "80")) {
  17.         $p = '';
  18.     } else {
  19.         $p = ':' . $port;
  20.     }
  21.  
  22.     return "http$s://$host$p$path";
  23. }
После чего формируемый адрес openid-сервера стал выглядеть нормально, а я продолжил свои эксперименты.

Затем я решил посмотреть, как выглядит база данных, какие в ней появились таблички и что где хранится. Оказалось, чтобы базы данных нет. Т.е. указанное при конфигурировании значение нигде не используется. Пришлось создать базу руками (и немного побеспокоиться, какая нужна кодировка для работы этого чуда?).
  1. CREATE DATABASE openidbase
  2.     CHARACTER SET 'utf8'
  3.     COLLATE 'utf8_general_ci';
Повторный запуск сервера привел к созданию двух таблиц с именами oid_associations и oid_nonces. Вот их код:
  1. CREATE TABLE `oid_nonces` (
  2.   `server_url` varchar(2047) DEFAULT NULL,
  3.   `timestamp` int(11) DEFAULT NULL,
  4.   `salt` char(40) DEFAULT NULL,
  5.   UNIQUE KEY `server_url` (`server_url`(255),`timestamp`,`salt`)
  6. ) ENGINE=InnoDB DEFAULT CHARSET=utf8
  7.  
  8. CREATE TABLE `oid_associations` (
  9.   `server_url` blob NOT NULL,
  10.   `handle` varchar(255) NOT NULL DEFAULT '',
  11.   `secret` blob,
  12.   `issued` int(11) DEFAULT NULL,
  13.   `lifetime` int(11) DEFAULT NULL,
  14.   `assoc_type` varchar(64) DEFAULT NULL,
  15.   PRIMARY KEY (`server_url`(255),`handle`)
  16. ) ENGINE=InnoDB DEFAULT CHARSET=utf8
С первого взгляда не разберешся для чего эти таблицы предназначены - будем читать мануал.

Также я заинтересовался тем, где можно настроить их имена (например, поменять префикс имени таблицы). В поисках этого места я открыл файл "openid\Auth\OpenID\MySQLStore.php" и увидел незамысловатый код DDL для этих двух таблиц (кодировку они наследуют от кодировки базы данных, и никакого кода выбора кодировки подключения я не нашел). Интересно, что будет, когда база в utf8, а кодировка подключения по-умолчанию в cp1251 (как у меня на хостинге).

Немного полезной информации нашлось и в файле "openid\Auth\OpenID\SQLStore.php" (это код класса от которого наследуются все классы работающие с конкретными СУБД, например, mysql, sqlite). В конструкторе SQLStore можно установить значения ряда переменных, в которых хранятся имена таблиц БД. В том же каталоге был обнаружен файл PostgreSQLStore.php. Так что, я полагаю, к трем вариантам установки openid можно добавить и четвертый, хранящий данные в postgres (непонятно почему он был скрыт на стадии установки), хотя экспериментировать я не стал. Бегло просмотрев еще несколько файлов библиотеки (они довольно неплохо документированы), я решил, что если возникнут проблемы, то "обработать напильником" не составит больших трудностей. В папке openid/contrib/upgrade-store-1.1-to-2.0 нашелся файл сценария python служащий для обновления структуры базы данных с версии 1.1 до 2.0. Правда, все обновление свелось к удалению таблицы nonces и созданию ее с нуля (Поставил себе заметку разобраться с тем, что же реально хранится в этих таблицах).

Когда я просмотрел содержимое таблиц, созданных для работы сервера, то меня смутило то, что только в таблице oid_associations появилась одна (и, причем совершенно непонятная запись). Таблица же oid_nonces оставалась пустой несмотря ни на что. Хотя я пару раз регистрировался и разрегистрировался на странице server.php. Решил, что, наверное, большего от примера создания openid-сервера ждать и не стоит (сервер не содержит никакого кода с примером регистрации пользователей, просто подтверждал имена всех пользователей, какие бы я не пробовал).

OpenID & Mediawiki



Теперь попробуем прикрутить openid к mediawiki. Расширение я загрузил из SVN репозитория http://svn.wikimedia.org/viewvc/mediawiki/trunk/extensions/OpenID/.

Это полноценный пример openid-сервера (который интегрирован с "родной" для mediawiki системой регистрации), так что теперь на персональной странице пользователя будет помещен тот самый html-код openid-провайдера.

Также расширение позволяет выполнять вход пользователей с других openid-сайтов. Если вы в форме аутентификации укажите свой openid-логин, например, с живого журнала, то будет выполнен вход в mediawiki-сайт, плюс, автоматически создана для вас учетная запись.

Начал я с того, что создал в базе (база для mediawiki, про старую базу с примером простенького сервера забудьте) новую таблицу (замените $wgDBprefix на используемый вами префикс mediawiki)
  1. CREATE TABLE /*$wgDBprefix*/user_openid (
  2.   uoi_openid varchar(255) NOT NULL,
  3.   uoi_user int(5) UNSIGNED NOT NULL,
  4.  
  5.   PRIMARY KEY uoi_openid (uoi_openid),
  6.   UNIQUE INDEX uoi_user (uoi_user)
  7. ) TYPE=InnoDB;
После чего скопировал в папку extensions каталог с кодом расширения и указал в файле LocalSettings.php путь к файлу OpenID.php
  1. require_once("$IP/extensions/OpenID/OpenID.php");
Для корректно работы расширения ему нужна библиотекa openid (онa не идет в поставке с самим расширением). К счастью, эти библиотеки были использованы в прошлых примерах, так что мне нужно было только подключить их к include_path. Так что в файле LocalSettings.php перед строкой загружающей расширение "extensions/OpenID/OpenID.php" была добавлена строчка, с указанием того, где можно найти файлы библиотеки openid:
  1. set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/extensions/OpenID/includes/');
Теперь я открываю заглавную страницу mediawiki и вижу изменения. Вверху страницы появилась кнопка "Login with OpenID", а в списке специальных страниц добавилась новая страница, содержащая форму аутентификации с помощью openid (Special:OpenIDLogin). Зайдя на свою персональную страницу, в коде html вижу ссылку на провайдера openid-услуг.



Попробуем войти в mediawik-у с помощью openid-имени. Жму на кнопку "Login with OpenID", затем попадаю на следующую форму:



Указываю URL в форме адрес, взятый из предыдущего примера (тот самый куцый oppenid-сервер, который только и умеет, что доверять всем и каждому).
 http : // center:89/openid/examples/server/server.php/idpage?user=vasyano
Жму на кнопку "Представиться системе" и ... получаю сообщение об ошибке.



Ладно, раз у меня на test-машине windows, то придется исправить в этом файле значение переменной на null. Открываю файл Auth/OpenID/CryptUtil.php, нахожу в нем следующий фрагмент и устанавливаю значение переменной Auth_OpenID_RAND_SOURCE равной null.
  1. if (!defined('Auth_OpenID_RAND_SOURCE')) {
  2.     /**
  3.      * The filename for a source of random bytes. Define this yourself
  4.      * if you have a different source of randomness.
  5.      */
  6.     //define('Auth_OpenID_RAND_SOURCE', '/dev/urandom');
  7.     define('Auth_OpenID_RAND_SOURCE', null);
  8. }
Повторно пробую пройти процедуру аутентификации после исправления ошибки и получаю:



Все, как и должно быть. Форма подтверждения аутентификации и запрос на то, хочу ли я доверять сайту, запросившему услугу openid-аутентификации. Правда, настроение подпортила появившаяся непонятно откуда в корне диска (того, где находится веб-каталог) папка tmp. Внутри, которой оказалось множество файлов, созданных openid расширением к mediawiki. Придется еще раз пройтись по настройке openid-расширения и указать для этих (безусловно, нужных для работы) файлов другое местоположение.

Как я писал выше, расширение для работы с openid имеет два назначение: вы можете входить на mediawiki-сайт с помощью openid-учетных данных предоставляемых некоторым openid-провайдером; либо ваша персональная страница на wiki будет использована для входа на другие сервера. Проще говоря, это расширение играет роль и клиента openid и сервера openid. Для работы каждого из них нужно настроить место, где будут храниться "какие то важные данные". Начну с клиентской части. В принципе, есть всего два варианта: либо файловая система, либо memcached-сервер. На целевом хостинге в internet memcached-ом даже и не пахло - придется обойтись файлами для хранения. Заметка на будущее: почему процедура аутентификации так тормозит (apache скушал весь процессор и не на одну секунду)?.

Добавляю в файл LocalSettings.php следующие переменные:
  1. $wgOpenIDConsumerStoreType = 'file';
  2. $wgOpenIDConsumerStorePath = dirname(__FILE__). '/tmp/';
После чего получил очередное сообщение об ошибке:
Fatal error: Failed to initialize OpenID file store in 
/home/c:/mediawiki\tmp\ 
in /c:/mediawiki\extensions\OpenID\includes\Auth\OpenID\FileStore.php 
on line 73
После непродолжительной медитации оказалось, что я поставил лишний пробел в имени каталога. Так что, после исправления openid-аутентификация заработала, и лишние папки в корне диска не создавались. Мораль: указывайте пути к файловому хранилищу правильно. Автоматически они не создаются (по крайней мере, тогда, когда нужно).

Заработать, то заработало, только после положительного ответа на "доверяю ли я этому сайту?", снова возникло сообщение об ошибке.И на этот раз более интересное:



Оказалось, что ответ openid-провайдера не совпадает с тем, что ожидал Consumer на стороне mediawiki. Ради интереса я вывел на экран, что же там вернулось от сервера.



Ага, возвращается массив с кучей информации, а клиент хочет получить всего лишь одну строку (интересно, с чем?). Пришлось заняться чтением документации и где и нашлась роковая фраза:
The software depends on the 1.x version of the OpenIDEnabled.com PHP library for OpenID.
:
Note: the 2.x version of the library will break this extension. 
A future version of the extension will work with the 2.x version correctly.
Я, конечно, страшно расстроился (сначала читать мануал - потом пробовать) и отправился искать в internet либо последнюю версию расширения openid (умеющую работать со второй версией этого протокола), либо старую версию (1.x) для openid-провайдера. Заметка для себя: что будет в планах с openid версии 1.

В ходе поисков обнаружилось, что вторая версия openid-протокола была выпущена в декабре 2007 г. и поддержки для нее в расширении mediawiki еще нет. Пришлось загрузить версию openid 1.2.3. Самое удивительное в том, что никаких больше действия (после замены библиотеки) мне не пришлось делать: все заработало с первого раза. Еще одна попытка аутентификации через openid прошла успешно (правда mediawiki переключила при этом локализацию интерфейса на тарабарский язык, но я решил, что следует провести боевой тест с использованием настоящего openid-сервер, того же живого журнала).

Посмотрел содержимое таблицы user_openid (как вы помните, ее я создал на стадии установки openid-расширения). В таблице оказались перечислены все использованные для входа внутрь wiki openid-адреса. И каждому из них был поставлен в соответсвие автоматически созданный пользователь самой mediawiki.



В следующий раз я раскажу про то, как создать клиент (Consumer) услуг openid-аутентификации. Ведь мне нужно интегрировать форму логина в flash-клиент обучающей машины. Напишу как только разберусь с этим процессом ...