![]() |
![]() |
|
||
![]() |
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [8 декабря 2009 г.] |
|
OAuth популярный протокол, который позволяет социальным сервисам интегрироваться между собой и дает безопасный способ обмена персональной информацией. OAuth может связать между собой 2 сервиса, каждый из которых имеет свою пользовательскую
Вернемся в 2005-й год и представим, что мы пишем социальную сеть. В ней имеется форма импорта контактов из адресной книги GMail. Что нужно для доступа к контактам GMail? Конечно, логин и пароль от ящика. Но если мы попросим ввести их на нашем сайте, пользователь заподозрит неладное. Где гарантия, что мы не сохраняем на сервере введенные пароли? Поэтому нам хочется, чтобы пароль вводился только на сайте GMail, и после этого доступ к контактам через API GMail предоставлялся нашей социальной сети (возможно, на время).
Это выглядит следующим образом: форма состоит из единственной |
Договоримся о терминах.
Сейчас я прошу вас закрыть листом бумаги верхнюю часть экрана и в качестве упражнения ответить на вопросы: кто такой Service Provider? что такое Protected Resource? кто такой Consumer и чем он отличается от User-а? где располагается API? Далее в статье мы свободно оперируем этими терминами. Если вы сейчас недостаточно хорошо в них ориентируетесь, могут быть проблемы с пониманием. |
Задача OAuth сделать так, чтобы User имел возможность работать на сервисе Consumer (в соцсети) с защищенными данными Service Provider-а (GMail), вводя пароль к этим данным исключительно на Service Provider-e и оставаясь при этом на сайте Consumer-а. Не так уж и сложно, верно?
OAuth часто называют «протоколом для роботов», в отличие от OpenID - «протокола для пользователей». Не путайте их!
Милицейская аналогияПредставьте, что
Как видите, OpenID и |
Прежде чем перейти к основной части, давайте посмотрим, как именно мы будем двигаться.
Хороший способ понять что-
Во-первых, напишем саму форму импорта контактов с GMail:
| Листинг 1: Велоформа импорта контактов |
<form action="http://gmail.com/auth.php?retpath=http://oursocialnetwork.ru/import.php" method="get"> <input type="submit" value="Загрузить адресную книгу" /> </form> |
Далее попросим разработчиков GMail сделать так, чтобы при переходе пользователя по URI /auth.php ему бы выдавалась форма авторизации (в нашем веломире GMail написан на PHP). После успешного ввода пароля пользователь должен редиректиться на сайт, чей URL указан в параметре retpath. Также дополнительно в URL должен передаваться некоторый секретный ключ, который уже можно использовать для доступа к API GMail.
Итак, после ввода пароля пользователь будет возвращаться к нам на сайт по следующему адресу:
| Листинг 2: Велоадрес возврата с велоключом |
http://oursocialnetwork.ru/import.php?secret=Y49xdN0Zo2B5v0RR |
А мы из скрипта /import.php обратимся к API GMail, передадим в него ключ Y49xdN0Zo2B5v0RR и загрузим контакты:
| Листинг 3: Запуск метода велоAPI |
$contacts = $gmailApi->getContacts($_GET['secret']); |
Ну что же, давайте теперь считать грабли (потому что шишки считать будет уже поздно).
Ну конечно же, вы догадались, что злоумышленник на своем сайте первым делом разместит ссылку
| Листинг 4: Ссылка на сайте злоумышленника |
http://gmail.com/auth.php?retpath=http://hackersite.ru/save.php |
и заставит вас на нее кликнуть. В результате он получит секретный ключ, который вернул GMail, а значит, и ваши контакты:
| Листинг 5: Велосекрет в адресе возврата |
http://hackersite.ru/save.php?secret=Y49xdN0Zo2B5v0RR |
Предположим, мы как-то защитили retpath, и он теперь может указывать только на наш сайт. Но проблема с параметром secret остается.
| Листинг 6: Велоадрес возврата с велоключом |
http://oursocialnetwork.ru/import.php?secret=Y49xdN0Zo2B5v0RR |
Secret можно подсмотреть из-за спины или перехватить методом прослушивания WiFi-трафика. Или на вашем сайте когда-нибудь найдется XSS-уязвимость, позволяющая "утянуть" секретный ключ. Имея значение secret, злоумышленник сможет прочитать вашу адресную книгу. Значит, нужно обезопасить secret от перехвата (в
Нужно помнить, что секретный ключ передается не только в URL, но еще и при вызове API-методов. Там тоже возможен перехват. Конечно, использование SSL здесь помогает. |
Если для каждого вызова API требуется разный secret, то нам придется организовывать столько редиректов на сайт Service Provider-а, сколько у нас вызовов. При интенсивном использовании API это работает очень медленно, да и неудобно порядком...
GMail, конечно, хочет знать, кто пользуется его API. Разрешить доступ одним сайтам и
Примечательно, что "подводных граблей" осталось еще много. Я не буду их здесь описывать, потому что эти грабли лежат в Марианской впадине (глубоко, 10920 м). На описание уязвимостей пришлось бы потратить с десяток страниц. Так что я сразу перейду к описанию OAuth, где все проблемы уже решены.
Есть замечательный цикл статей про OAuth: Beginner's Guide to OAuth (на английском; от автора с говорящим прозвищем hueniverse). Его изучение отнимет у вас приблизительно 4 часа, если вы до этого момента совершенно не знакомы с темой. |
При работе с OAuth важно, что термин Consumer не ограничивается смыслом "сайт".
Но из одного OAuth каши не сваришь. Действительно, все, что дает OAuth, — это возможность авторизоваться на удаленном сервисе (Service Provider) и делать автризованные запросы к API. Не важно, как устроен этот API: это может быть чистый SOAP, REST-подход т. д. Главное, чтобы каждый метод API принимал на вход специальные параметры, передаваемые согласно протоколу OAuth.
Один из принципов OAuth гласит, что никакие секретные ключи не должны передаваться в запросах открытыми (выше в примере мы рассматривали, почему). Поэтому протокол оперирует понятием Token. Токен очень похож на пару логин + пароль:
Итак, если Consumer и Provider каким-то образом договорятся между собой о Shared Secret, они могут открыто обмениваться в URL соответствующими ключами (Key), не опасаясь, что перехват этих ключей будет опасен. Но как защитить URL с Key от подделки?
"Цифровая
Аналогично, цифровая подпись добавляется к некоторому блоку данных, удостоверяя: тот, кто сформировал эти данные, не выдает себя за другого. Цифровая подпись не шифрует документ, она лишь гарантирует его подлинность! Поставить подпись позволяет тот самый Shared Secret, который известен получателю и отправителю, но более никому.
Как это работает? Пусть наш
$transfer = $message . "-" . md5($message . $sharedSecret);
// $transfer = "Мой телефон 1234567" . "-" . md5("Мой телефон 1234567" . "529AeGWg")
$signatureToMatch = md5($message . $sharedSecret);
// $signatureToMatch = md5("Мой телефон 1234567" . "529AeGWg");
Дальше остается только сравнить получившееся значение Итак, чтобы сформировать MD5-подпись, обязательно знать Shared Secret. (Кстати, кроме MD5 есть и другие алгоритмы необратимого хэширования.) Злоумышленник не знает Shared Secret, поэтому и подпись он подделать не может. |
Чтобы "вживую пощупать" OAuth, нам потребуются две вещи:
Глядя на код демо-скрипта и читая пояснения ниже в статье, можно разобраться с деталями протокола.

Вы можете вставить данный виджет на любой PHP-сайт, просто скопировав его код и подправив верстку. Выводятся все твиты с сервиса РуТвит, помеченные указанным хэш-тэгом, а также имеется возможность добавлять новые твиты (тут-то как раз и задействуется OAuth). Виджет использует API и OAuth-авторизацию РуТвита, которые, кстати говоря, совпадают со стандартом API Twitter-а.
В настоящий момент для работы с OAuth в PHP есть только одна сколь-нибудь универсальная и библиотека: OAuth.php by Andy Smith. У нее два недостатка: она написана грязно, и она не обновлялась уже больше года. Ссылки на другие библиотеки приведены на сайте OAuth, однако эти инструменты либо требуют установки PHP extension, либо еще слишком сыры, либо же имеют обширные внешние зависимости от других библиотек (хотя черновик библиотеки для Zend Framework выглядит очень перспективно). Так что, как говорится, "мышки плакали, кололись, но продолжали есть |
Вы можете запустить этот скрипт на своем тестовом сервере. Для этого нужно выполнить три действия:
Скрипт специально написан без ООП и максимально «в лоб». Преследовались две цели: а) добиться краткости и понятности кода, б) сделать код идущим параллельно линии повествования в статье (отсюда этот конечный автомат и switch ... case). Да, и еще одно. Файл |
Поговорим о том, откуда появляются приложения и как Service Provider о них узнает. Все достаточно просто: Service Provider имеет специальную форму регистрации приложений, которой может воспользоваться любой желающий. Вот пример такой формы:

После регистрации приложения вам выдается 5 параметров, которые требуются для работы с OAuth. Вот как они могут выглядеть:

Здесь Consumer key и Consumer
| Листинг 7: Параметры OAuth и определение переменных |
<?php
require_once "OAuth.php";
// Разные параметры.
define("ENCODING", "windows-1251"); // Кодировка сайта. Если у вас UTF-8, то вы молодец!
define("TAG", "support"); // Тэг, по которому производится фильтрация твитов.
// Параметры OAuth. Запомните их наизусть (особенно SECRET).
define("OA_CONSUMER_KEY", "JId0zVAbQCVnqjD9OlvM"); // Параметры OAuth-доступа.
define("OA_CONSUMER_SECRET", "qocMBQg1P17CBcdVsJizsNPnlGbTU4fvlGxAszmzB5");
define("OA_URL_REQ_TOK", "http://api.rutvit.ru/oauth/request_token");
define("OA_URL_AUTH_TOK", "https://api.rutvit.ru/oauth/authorize");
define("OA_URL_ACCESS_TOK", "http://api.rutvit.ru/oauth/access_token"); |
...или, в переводе на великий могучий:
|
В примере с GMail мы использовали 2 вида удаленных вызовов: а) редирект через браузер; б) обращение к API изнутри скрипта.
И мы вскрыли ряд проблем с безопасностью, что наводит на мысль: вызовов должно быть больше. Так и происходит в OAuth: добавляются еще промежуточные запросы от скрипта Consumer-а к Provider-у, оперирующие токенами. Давайте их рассмотрим.
| Листинг 8: Обрабатываем смену состояний через конечный автомат |
// Для работы с OAuth нам требуется 3 переменные, сохраняющие свои значения
// между загрузками страниц (для простоты - храним их в сессии).
session_start();
$S_MSG = &$_SESSION['msg'];
$S_REQUEST_TOK = &$_SESSION['REQUEST_TOK'];
$S_ACCESS_TOK = &$_SESSION['ACCESS_TOK'];
// Путь:
// form_is_sent ->
// fetch_request_token ->
// authorize_request_token (через браузер) ->
// fetch_access_token (обмен request_token на access_token) ->
// send_msg (через API)
// Или:
// form_is_sent ->
// send_msg (через API)
$action = @$_GET['action'];
while ($action) {
switch ($action) { |
| Листинг 9: Обработка отправки формы |
// 1. Запрошена отправка формы. Определяем, с какого шага начинать:
// либо с OAuth, либо с отправки сообщения через API.
case 'form_is_sent': {
// Сохраняем сообщение в сессию, оно нам понадобится позже.
$S_MSG = $_POST['msg'];
if ($S_ACCESS_TOK && $S_ACCESS_TOK->secret) {
// Пользователь уже отправлял комментарии в текущей сессии.
$action = 'send_msg';
} else {
// Авторизация еще не проведена, запускаем процедуру OAuth.
$action = 'fetch_request_token';
}
break;
} |
| Листинг 10: Fetch Request Token |
// 2. Запрошено получение Request Token.
// Обращаемся к Service Provider через сокет и получаем токен.
case 'fetch_request_token': {
// Формируем запрос на получение Request Token.
$consumer = new OAuthConsumer(OA_CONSUMER_KEY, OA_CONSUMER_SECRET);
$req = OAuthRequest::from_consumer_and_token(
$consumer, NULL,
"GET", "http://api.rutvit.ru/oauth/request_token"
);
// Добавляем в запрос цифровую подпись, чтобы не подделали.
$req->sign_request(new OAuthSignatureMethod_HMAC_SHA1(), $consumer, NULL);
// Получаем Request Token и отправляем его на авторизацию.
$parsed = OAuthUtil::parse_parameters(file_get_contents($req->to_url()));
$S_REQUEST_TOK = new OAuthToken($parsed['oauth_token'], $parsed['oauth_token_secret']);
// Переходим к следующему состоянию.
$action = 'authorize_request_token';
break;
} |
| Листинг 11: Redirect to Authorization |
// 3. Авторизация (подтверждение пользователем) Request Token's через редирект.
// Переадресуем браузер на Service Provider для продтверждения доступа пользователем.
// При возврате обратно в GET-параметрах будет action=fetch_access_token.
case 'authorize_request_token': {
// На этот URL вернется браузер после подтверждения.
$callbackUrl = "http://{$_SERVER['HTTP_HOST']}{$_SERVER['SCRIPT_NAME']}"
. "?action=fetch_access_token";
// Передаем callback-URL в параметрах (протокол OAuth 1.0; в 1.0a - уже не так!).
$authUrl = "http://api.rutvit.ru/oauth/authorize" . "?"
. "&oauth_token={$S_REQUEST_TOK->key}"
. "&oauth_callback=" . urlencode($callbackUrl);
// Браузерный редирект.
header("Location: $authUrl");
exit();
} |
| Листинг 12: Fetch Access Token |
// 4. Обмен Request Token на Access Token и запись Access Token в сессию.
// Сюда вернулись из редиректа после подтверждения доступа пользователем.
case 'fetch_access_token': {
$consumer = new OAuthConsumer(OA_CONSUMER_KEY, OA_CONSUMER_SECRET);
$req = OAuthRequest::from_consumer_and_token(
$consumer, $S_REQUEST_TOK,
"GET", "http://api.rutvit.ru/oauth/access_token",
array() // доп. параметры
);
$req->sign_request(new OAuthSignatureMethod_HMAC_SHA1(), $consumer, $S_REQUEST_TOK);
// Выполняем запрос и записываем Access Token в сессию.
$parsed = OAuthUtil::parse_parameters(file_get_contents($req->to_url()));
$S_ACCESS_TOK = new OAuthToken($parsed['oauth_token'], $parsed['oauth_token_secret']);
// Переход к отправке сообщения.
$action = 'send_msg';
break;
} |
| Листинг 13: Call API |
// 5. Отправляем сообщение.
// Оборачиваем URL API в OAuth-контейнер.
case 'send_msg': {
$consumer = new OAuthConsumer(OA_CONSUMER_KEY, OA_CONSUMER_SECRET);
$req = OAuthRequest::from_consumer_and_token(
$consumer, $S_ACCESS_TOK,
'POST', 'http://api.rutvit.ru/statuses/update.xml',
array('status' => "#" . TAG . " " . iconv(ENCODING, "UTF-8", $S_MSG))
);
$req->sign_request(new OAuthSignatureMethod_HMAC_SHA1(), $consumer, $S_ACCESS_TOK);
// Отправляем POST-запрос.
$h = curl_init();
curl_setopt($h, CURLOPT_URL, $req->get_normalized_http_url());
curl_setopt($h, CURLOPT_POST, true);
curl_setopt($h, CURLOPT_RETURNTRANSFER, true);
curl_setopt($h, CURLOPT_POSTFIELDS, $req->to_postdata());
$resp = curl_exec($h);
$code = curl_getinfo($h, CURLINFO_HTTP_CODE);
// При успехе - редирект обратно на страницу с виджетом.
if ($code != 200) {
e($resp);
exit();
}
header("Location: {$_SERVER['SCRIPT_NAME']}");
exit();
} |
Окончание скрипта должно быть понятно и без подробных разъяснений.
| Листинг 14: Окончание скрипта: вывод виджета |
// конец case
}
}
// Получаем все имеющиеся твиты.
$text = file_get_contents("http://api.rutvit.ru/search.xml?rpp=5&q=" . urlencode("#" . TAG));
$TWEETS = new SimpleXMLElement($text);
// Shortcut для вывода сообщения с перекодировкой и квотингом.
function e($text, $quote = 1)
{
$text = iconv("utf-8", ENCODING, $text);
echo $quote? htmlspecialchars($text) : $text;
}
?>
<style>
.hiddenLink { display: none }
</style>
<div style="border: 1px solid black; padding: 0.5em">
<?foreach ($TWEETS->status as $tweet) {?>
<div style="margin-bottom: 6px">
<b><?e($tweet->user->screen_name)?>:</b>
<?e($tweet->text_formatted, 0)?>
</div>
<?}?>
<form method="post" action="<?e($_SERVER['SCRIPT_NAME'])?>?action=form_is_sent" style="margin: 1em 0 0 0">
<input type="text" size="30" name="msg" />
<input type="submit" value="Отправить" />
</form>
</div> |
См. также:
![]() |
| ||||||||||||||||||||||||||||
| Дмитрий Котеров | 8 декабря 2009 г. ©1999-2010 | | Контакт | Вернуться к оглавлению |