OAuth на практике. Аутентификация и авторизация пользователей сайта через популярные социалки

в 14:51, , рубрики: api, Facebook API, Google API, mail.ru api, oauth, twitter api, Yandex API, Веб-разработка, Вконтакте API, метки: , , , , , ,

Думаю, не мне одному чрезвычайно надоели ресурсы, требующие регистрации по каждому поводу и без. С обязательной капчей, которая правильно введется только с пятого раза, с подтверждением по е-мейлу, которое обязательно свалится в спам и то — только через сутки. Придумывать каждый раз новую пару логин-пароль — забудется, вводить одно и то же на всех сайтах — небезопасно. Местами прокатывают пары вида «qwerty:qwerty» или «login:password», но, увы, далеко не везде. Надоело. Не счесть, сколько раз я, увидев надпись «только зарегистрированный пользователь может ****», просто кривился и закрывал вкладку, чтобы больше ни разу на этот сайт не заходить. Неужели администраторы ресурсов сами этого не понимают? А ведь технологии OAuth уже 6 лет, и 90% процентов интернет-аудитории имеет аккаунт минимум на одном из ресурсов, поддерживающих OAuth. Так почему кнопки «Войти через» нет на каждом ресурсе? Я установил опрос у себя на сайimageте. Проголосовало немного (далеко не все мои пользователи имеют собственный сайт с регистрацией), но достаточно, чтобы появилась мысль написать эту статью, в надежде, что сайтов, использующих OAuth для авторизации, станет больше. Потому что:

  • Это не сложная технология. Смею надеяться, что каждый, нашедший в себе силы прочитать статью до конца, сможет самостоятельно прикрутить OAuth к своему сайту за 1-2 дня.
  • OAuth базируется на HTTP. Большинство сервис-провайдеров даже цифровой подписи не требуют, т.е., кроме собственно возможности создавать GET и POST запросы — ничего не надо, а простота протокола позволяет написать OAuth-клиент «на коленке» буквально за час. Без каких-то специальных библиотек.
  • Так они и зарегистрируются у вас! Просто используют для этого свои логин-пароль на другом сайте. Сервис-провайдеры не предоставляют какую-то специфическую характеристику пользователя, необходимую на вашем сайте? Так запросите эту характеристику при первом визите и всех делов. В остальном — пользователь, вошедший через OAuth, ничем не отличается от зарегистрировавшихся обычным путем.
  • Я не ставил целью подробно описать протокол OAuth и его возможности. Это все уже написано неоднократно, например, здесь и здесь. Я хочу дать примеры практической реализации OAuth для наиболее распространенных социалок рунета без привязки к конкретному языку.
  • Ну а что касается 7%, которым OAuth не нужен — если 93 из ста прочитавших этот пост админов внедрят OAuth у себя на сайте, я буду более чем доволен :)

Почему OAuth, а не OpenID?

Потому что OpenID практически бесполезен для тех целей, для которых декларирован. Это сугубо мое мнение, но оно опирается не на пустое место.
Во-первых, OpenID пользуются в основном «гики», процент которых в интернете не настолько высок, чтобы перестраивать под них сайты (за некоторым исключением, разумеется :) ) Почему так? Потому что для того, чтобы получить OpenID аккаунт, надо — его получить. Зайти на OpenID сервер и предпринять некоторые действия, чтобы заполучить некий, довольно невразумительный, набор символов. Который, несмотря на сложность (для простого пользователя) надо не забыть и вводить при случае, если на глаза попадется знакомая пиктограмма image. Ну какой казуальный пользователь будет это делать? А с OAuth все намного проще — видит человек кнопку «Войти через ВКонтакте», нажимает, и… уже на сайте с правами зарегистрированного пользователя. «Будьте проще», — говорил классик, — «и люди к вам потянутся». Как в воду глядел.
Во-вторых, возможности OAuth далеко не исчерпываются аутентификацией и авторизацией. Получив в процессе авторизации токен, его можно использовать для дальнейшей интеграции возможностей социалки в свой ресурс — чтение/написание постов, доступ к френдленте и стене и многое другое.
В-третьих, OpenID активно пользуются спамеры и хакеры. Зачастую реализация OpenID аутентификации на ресурсе делается без особого внимания к известным его уязвимостям — по одному только описанию протокола на OpenID-провайдере или с помощью неизвестно кем и когда написанной библиотеки. К примеру, многие сайты не требуют со входящих по OpenID ввода капчи. И ничто не мешает злоумышленнику поднять свой OpenID-сервер, который будет подтверждать любой идентификатор и начать спамить доверчивый сайт автоматически сгенеренными идентификаторами. Кроме того, OpenID аутентификация, в сущности, не дает никаких гарантий клиенту. Она подтверждает лишь, что запрашиваемый пользователь действительно зарегистрирован на одном из OpenID-серверов — и все. Механизмы получения дополнительной информации о пользователе (email, имя, возраст) имеются, но мало кем поддерживаются. Можно, конечно, в нарушение идей OpenID, анализировать идентификатор и доверять только определенным OpenID-провайдерам (например, тем же социалкам). Но смысл, если почти все из них (за исключением ЖЖ, но он и по OpenID ровным счетом ничего не расскажет о пользователе) поддерживают OAuth, который позволяет получить намного больше информации? Так что со всех своих сайтов я вообще убрал поддержку OpenID.

Почему OAuth, а не виджет «Войти через»?

Нет проблем. Если вас устраивают функционал, дизайн и уровень безопасности виджета — то его воткнуть в код на самом деле намного проще — можете дальше не читать.

Как это работает.

Если кому интересны все подробности, то см. ссылки выше. А вкратце — так:

  1. Заходите на сервис-провайдер ХХХ, регистрируете там свой сайт, получаете код клиента и секретный код.
  2. По нажатию кнопки на вашем сайте «войти через ХХХ» производится обмен запросами с сервером XXX, перенаправление пользователя на ХХХ для ввода пароля, логина и подтверждения передачи данных на ваш сайт и получения токена, с помощью которого у сервера ХХХ можно запросить дополнительные данные о пользователе. Конкретика зависит от реализации OAuth на сервис-провайдере — поддерживаемой версии протокола, поддерживаемых потоков и пр.
  3. С помощью полученного токена с сервера ХХХ получаются имя, емайл, дата рождения, аватар и прочая информация, которая обычно и требуется при регистрации. После этого можно вызывать стандартную функцию по добавлению нового пользователя.

Почему сырой HTTP, а не куски кода с использованием OAuth библиотеки?

Потому что я порядком помучался, вытаскивая этот самый сырой HTTP зачастую и из кусков кода, иллюстрирующих использование той или иной библиотеки. Конкретная реализация OAuth на каждом сервере имеет свои тонкости, перед которыми может спасовать даже самая гибкая библиотека. Кроме того, веб-языков много, библиотек — еще больше, дать примеры на все просто нереально. А сырого HTTP (при наличии мозгов) вполне достаточно для использования любой библиотеки. Чего не скажешь об обратном случае.

Практические рекомендации по реализации

Разумеется, в первую очередь надо зарегистрироваться в соцсети, активировать аккаунт, ну и всё такое. Не торопиться. Некоторые сервера не сразу корректно обрабатывают запросы от свежезарегистрированных OAuth-клиентов. Здесь я расписал только успешные потоки, забывать про обработку ошибок — никак не стоит. Также я практически не уделил внимания аспектам безопасности — это тема отдельной статьи. Как минимум, везде, где можно генерировать callback-url для каждого пользователя — это стоит делать (увы, можно мало где), как и использользовать уникальный код в параметре scope (там, где этот параметр поддерживается). Я все это пока оставил за скобками.

imageВКонтакте

1. Идем сюда. Тип — «Веб-сайт». Вводим базовый домен и адрес сайта. На странице настроек получаем client_id (ID приложения) и secret_key (защищенный ключ). image2. Втыкаем в код кнопку вида

<a href="http://oauth.vk.com/authorize?client_id={client_id}&redirect_uri=mysite.com/vklogin&response_type=code" title="Зайти через ВКонтакте">Зайти через ВКонтакте</a>

Почему response_type=code, а не token? Потому что некоторые ресурсы не поддерживают response_type=token. Что такое vklogin? Это скрипт, который должен будет обработать ответ от сервера ВКонтакте, получить от него токен и добыть информацию о пользователе.
3. Что делает vklogin?
3.1. vklogin получает в get-параметрах (прямо в строке запроса) результаты авторизации. В случае успеха

http://mysite.com/vklogin?code=7a6fa4dff77a228eeda56603b8f53806c883f011c40b72630bb50df056f6479e52a

В случае неудачи

http://mysite.com/vklogin?error=invalid_request&error_description=Invalid+display+parameter

Соответственно, первым делом скрипт анализирует наличие code, достает этот параметр и выполняет
3.2. https-запрос (протоколом полагается POST, но ВКонтакте используется GET. Разницы немного, но имейте в виду) вида:

GET https://oauth.vk.com/access_token?client_id={client_id}&client_secret={secret_key}& code=7a6fa4dff77a228eeda56603b8f53806c883f011c40b72630bb50df056f6479e52a//полученный в параметрах код

3.3. и анализирует ответ. Любой ответ, кроме 200 OK сигнализирует об ошибке. В случае успеха мы получаем в теле ответа список параметров в формате JSON, содержащий access_token и user_id. После чего (мы же регистрируем пользователя) выполняется еще один GET запрос — обращение к методу users.get API ВКонтакте:

GET https://api.vk.com/method/users.get?uids={user_id}&fields=uid,first_name,last_name,nickname,screen_name,sex,bdate,city,country,timezone,photo&access_token={access_token}

3.4. В ответ на это (опять же в случае успеха) мы получаем опять-таки JSON список перечисленных параметров. Что нам и нужно было. Имя пользователя — в utf8, город и регион — в виде кодов, названия надо получать вызовом отдельного метода с укаазнием кода.
Полный список того, чего можно запросить, можно взять здесь. Полный список методов API ВКонтакте здесь.

imageОдноклассники

1. Регистрируемся как разработчик здесь. Идем сюда и заполняем заявку на получение OAuth доступа. Н-да. Заявка, похоже, обрабатывается вручную. Если ответа в течение суток нет, пинаем поддержку по тому же адресу. «Одноклассники», что тут скажешь...
2. Получаем письмо с инструкциями и, следуя им, заполняем форму.image
Все заполненные в примере поля обязательны к заполнению. Нажимаем «Сохранить» и получаем письмо на указанный емайл, содержащее client_id(Application ID), public_key(Публичный ключ приложения) и secret_key(Секретный ключ приложения)
3. Код кнопки должен быть вида:

<a href="http://www.odnoklassniki.ru/oauth/authorize?client_id={client_id}&response_type=code&redirect_uri=http://mysite.com/oklogin" title="Одноклассники">Войти через Одноклассников</a>

4. oklogin
3.1. Анализируем строку на наличие code.
3.2. Выполняем POST запрос на:

http://api.odnoklassniki.ru/oauth/token.do

с параметрами в теле запроса:

code={code}&redirect_uri=http://mysite.com/oklogin&grant_type=authorization_code&client_id={client_id}&client_secret={secret_key}

3.3. Получаем JSON список параметров, содержащий access_token. выполняем GET-запрос:

GET http://api.odnoklassniki.ru/fb.do?method=users.getCurrentUser&access_token={access_token}&application_key={public_key}&sig={sign}

sign — трахнутая на все байты md5-подпись

sign=hex_md5('application_key={public_key}method=users.getCurrentUser'+hex_md5({access_token}+{secret_key}))

Да-да, часть подписи кодится дважды.
3.4. В ответ получаем JSON-список параметров, содержащий имя, ссылку на фото, день рождения и пр. Имя — utf8;
Полный список возвращаемых параметров функции users.getCurrentUser здесь. Описание API здесь. Но сразу предупреждаю, что автор его был болен рассеянным склерозом в последней стадии, поэтому о многих аспектах использования той или иной функции надо просто догадываться.

imageFacebook

1. Идем сюда и нажимаем «Создать новое приложение». В настройках в пункте App Domains вводим наш(и) домен(ы). Ставим галочку на «Website with Facebook Login» и вводим url нашего сайта. Записываем client_id (App ID) и secret_key (App Secret).imageНа вкладке «Auth Dialog» ставим галочку на «Authenticated Referrals» и тип «code» в «Auth Token Parameter». image
2. Кнопка на нашем сайте отправляет пользователя на:

https://www.facebook.com/dialog/oauth?client_id={client_id}&redirect_uri=mysite.com/fblogin&response_type=code

3.fblogin.
3.1. Анализируем строку на наличие code.
3.2. Выполняем GET запрос

GET https://graph.facebook.com/oauth/access_token?client_id={client_id}&redirect_uri=http://mysite.com/fblogin&client_secret={client_secret}&code={code}

3.3. Получаем в теле ответа список параметров в каноническом виде (param1=value1&param2=value2). Из полученного сейчас нам нужен только access_token, который мы вставляем в GET-запрос:

GET https://graph.facebook.com/me?access_token={access_token}

3.4. В ответ получаем JSON-список параметров, содержащий имя, ссылку на фото, день рождения и пр. Имя — NFC юникод (в виде u0410u0401u0419)
Список функций API (и возвращаемых данных) здесь.

imageTwitter

1. Идем сюда и регаем новое приложение. Вводим имя, описание, наш сайт и урл скрпита-обработчика. imageНа странице настроек нажимаем кнопку «Create my access token».image

Дальше слов нет, сплошные эмоции. Горечь и боль переполняют меня. Ну зачем же они так??? Рекомендую для начала натравить на twitter какую-нибудь OAuth-библиотеку. И только, если ничего не получится (лично у меня не получилось подружить Net::OAuth с твиттером) вооружаемся терпением и лезем разбираться.

1. Здесь OAuth 1.0 и тут всё веселее. При нажатии пользователем кнопки «Зайти через Twitter» мы запускаем скрипт, который выполняет POST-запрос на

https://api.twitter.com/oauth/request_token

содержащий следующий заголовок:

Authorization:OAuth oauth_callback=«http%3A%2F%2Fmysite.com%2Ftwlogin», oauth_consumer_key="{consumer_key}", oauth_nonce="{nonce}", oauth_signature_method=«HMAC-SHA1», oauth_timestamp="{time}", oauth_signature="{sign}", oauth_version=«1.0»

Здесь oauth_callback — escape-uri функции-обработчика ответа от сервера;
consumer_key берется со страницы настроек приложения;
nonce — строка случайных base64 символов, в принципе, любой длины, хотя полагается 32байта, строка при каждом запросе должна быть новой, иначе сервер вернет ошибку;
time — unix-время сервера (обратите внимание, чтобы часовой пояс на сервере был выставлен правильно, сервер твиттера заворачивает как «просроченные» запросы, так и запросы «из будущего»);
а sign — это цифровая подпись по HMAC-SHA1 методу.
Время и nonce надо запомнить перед началом составления запроса — в подписи и в самом запросе эти параметры должны совпадать.
1.1. Как собирается подпись. Берутся все параметры и значения из строки запроса и его тела, escape-кодятся, сортируются по имени параметра и соединяются в одну строку через '&'. Базовая строка составляется так:

{ВИД_ЗАПРОСА}

Таким образом, строка параметров escape-енкодится два раза. Ладно, у нас тут частный случай, всё это можно забиыть и просто вставить нужные параметры вот в это:

str='POST&https%3A%2F%2Fapi.twitter.com%2Foauth%2Frequest_token&oauth_callback%3D{http%253A%252F%252Fmysite.com%252Ftwlogin}%26oauth_consumer_key%3D{consumer_key}%26oauth_nonce%3D{random base64 srting}%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D{time}%26oauth_version%3D1.0'

1.2. Дальше эта строка передается в функцию, вычисляющую hmac_sha1 подпись (гуглим, если не знаем, что это) с базой 64. Что-то типа такого:

str1=hmac_sha1(str,Consumer_secret+'&'+OAuth_token_secret,64).

У нас пока OAuth token secret-а нет, поэтому кодируем мы строкой, содержащей Consumer secret с добавленным в конце знаком '&'.Результат (если он бинарный) base_64 енкодим, а потом еще и escape-енкодим. Это у нас получилась подпись. Вставляем её в заголовок запроса и отправляем.
2. Получаем от сервера ответ, в случае успеха содержащий в теле ответа oauth_token=XXXXX. Редиректим пользователя на

https://api.twitter.com/oauth/authenticate?oauth_token=XXXXX

3. После успешной авторизации сервер вызывает наш скрипт twlogin, который:
3.1. Проверяет строку на наличие oauth_token и oauth_verifier (авторизация успешна)
3.2. Выполняет POST-запрос на

https://api.twitter.com/oauth/access_token

содержащий заголовок

Authorization:OAuth oauth_consumer_key="{consumer_key}", oauth_nonce="{random base64 string}", oauth_signature_method=«HMAC-SHA1», oauth_timestamp="{time}", oauth_signature="{sign}", oauth_token="{oauth_token}", oauth_version=«1.0»'

и, в теле запроса:

oauth_verifier={oauth_verifier}

oauth_token и oauth_verifier берутся из строки параметров, с которыми вызван наш скрипт, sign считается уже описанным методом из строки

str='POST&https%3A%2F%2Fapi.twitter.com%2Foauth%2Faccess_token&oauth_consumer_key%3D{consumer_key}%26oauth_nonce%3D{random base64 string}%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D{time}%26oauth_token%3D{oauth_token}%26oauth_verifier%3D{oauth_verifier}%26oauth_version%3D1.0';

3.3. В случае успеха приходит ответ, содержащий в каноническом виде список параметров. Выполняем GET-запрос

GET https://api.twitter.com/1/users/lookup.json?user_id={user_id}

с заголовком

Authorization:OAuth oauth_consumer_key="{consumer_key}", oauth_nonce="{random base64 string}", oauth_signature_method=«HMAC-SHA1», oauth_timestamp="{time}", oauth_signature="{sign}", oauth_token="{oauth_token}", oauth_version=«1.0»'

user_id и oauth_token берем из пришедшего списка, подпись считается на базе строки

str='GET&https%3A%2F%2Fapi.twitter.com%2F1%2Flookup.json&oauth_consumer_key%3D{consumer_key}%26oauth_nonce%3D{random base64 string}%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D{time}%26oauth_token%3D{oauth_token}%26oauth_version%3D1.0%26user_id%3D{user_id}'

и, теперь уже полученного в параметрах, oauth_token_secret:

str1=hmac_sha1(str,Consumer_secret+'&'+OAuth_token_secret,64)

3.4. В случае успеха нам приходит JSON-список параметров, содержащий имя, фото и пр. Полный список функций API и их описания здесь.

imageMail.ru (МойМир)

1. Идем сюда и регистрируем сайт. Вводим название, урл сайта и урл файла receiver.html, который можно скачать тут же. receiver.html — файл, инициализирующий JS API, и, в нашем случае он не нужен, но сервер МайлРу проверяет наличие этого файла по указанному адресу и не дает закончить регистрацию, пока этот receiver.html не окажется там, где полагается. На странице настроек получаем client_id (ID) и secret_key (секретный ключ).image2. Код кнопки должен быть вида:

<a href="https://connect.mail.ru/oauth/authorize?client_id={client_id}&response_type=code&redirect_uri=http%3A%2F%2Fmysite.com%2Fmmlogin">Войти через MailRu</a>

3. mmlogin
3.1. Анализируем строку на наличие code.
3.2. Выполняем POST запрос на:

https://connect.mail.ru/oauth/token

с параметрами в теле запроса:

client_id={client_id}&client_secret={secret_key}&grant_type=authorization_code&code={code}&redirect_uri=http://mysite.com/mmlogin

3.3. Получаем JSON список параметров, содержащий access_token. выполняем GET-запрос:

GET http://www.appsmail.ru/platform/api?method=users.getInfo&secure=1&app_id={client_id}&session_key={access_token}&sig={sign}

sign — md5-подпись

sign=hex_md5('app_id={client_id}method=users.getInfosecure=1session_key={access_token}{secret_key}')

в виде hex string.
3.4. В ответ получаем JSON-список параметров, содержащий имя, ссылку на фото, день рождения и пр. Имя — utf8;
Список функций API (и описания) здесь.

imageЯндекс

1. Идем сюда. Выбираем все права Яндекс.логин. Вводим параметры. image
Получаем client_id(Id приложения) и secret_key(Пароль приложения).image
2. Код кнопки должен быть вида:

<a href="https://oauth.yandex.ru/authorize?response_type=code&client_id={client_id}" title="Яндекс">Войти через Яндекс</a>

3. yalogin
3.1. Анализируем строку на наличие code.
3.2. Выполняем POST запрос на:

https://oauth.yandex.ru/token

с параметрами в теле запроса:

grant_type=authorization_code&code={code}&client_id={client_id}&client_secret={secret_key}

3.3. Получаем JSON список параметров, содержащий access_token. выполняем GET-запрос:

GET https://login.yandex.ru/info?format=json&oauth_token={access_token}

3.4. В ответ получаем JSON-список параметров, содержащий имя, email, день рождения. (Немного, но яндекс.логин больше не дает. Можно попробовать запросить дополнительные параметры с яндекс.мойкруг — предварительно дав на это права на странице натроек, но рассчитывать на развернутый ответ не стоит — количество пользователей почты яндекса и «Моего Круга» отличается в порядки.) Имя — NFC-юникод. Кстати, здесь используемая мной json-библиотека вдруг заглючила. Оказалось, в ответе от яндекса двоеточие от имени параметра отделено пробелом,

email : xxx@ttt.com

чего не наблюдалось в ответах от других сайтов. Тут явно ошибка библиотеки, RFC на JSON не запрещает лишние пробелы, но на всякий случай обращаю внимание — вдруг кто-то еще споткнется на этом.
Описание API yandex.login здесь.

imageGoogle

1. Идем сюда. Создаем новый проект. На вкладке API access нажимаем кнопку «Create an OAuth2 client ID».imageПолучаем client_id(Client ID) и secret_key(Client Secret).image
2. Код кнопки должен быть вида:

<a href="https://accounts.google.com/o/oauth2/auth?redirect_uri=http%3A%2F%2Fmysite.com%2Fgglogin&response_type=code&client_id={client_id}&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile" title="Войти через Google">Войти через Google</a>

3. gglogin
3.1. Анализируем строку на наличие code.
3.2. Выполняем POST запрос на:

https://accounts.google.com/o/oauth2/token

с параметрами в теле запроса:

code={code}&client_id={client_id}&client_secret={secret_key}&redirect_uri=http://mysite.com/gglogin&grant_type=authorization_code

и обязательно указываем тип запроса

Content-type:application/x-www-form-urlencoded

3.3. Получаем JSON список параметров, содержащий access_token. выполняем GET-запрос:

GET https://www.googleapis.com/oauth2/v1/userinfo?access_token={access_token}

3.4. В ответ получаем JSON-список запрошенных параметров.
Описание API работы с гугл-контактами здесь.

На этом пока все. Думаю, даже самые нелюдимые пользователи рунета имеют аккаунт хотя бы на одном из вышеупомянутых серверов.
Надеюсь, статья поможет тем, кто хочет сделать на своем сайте регистрацию по OAuth. Во всяком случае, попадись мне такая статья раньше, я бы сэкономил кучу времени и сил.
Я реализовывал это всё на perl, соответственно, если у кого возникнут вопросы по реализации OAuth на перле — могу дать свои рекомендации. По другим языкам — увы.

Автор: skyranger

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js