Об OAuth

2017-02-05

Веб — странное место. Нормального входа по логину-паролю всем скоро стало не хватать. Сервисов слишком много, нормальный человек столько паролей не придумает. Поэтому появился OpenID.

OpenID — это распределённая система аутентификации в вебе. Пользователю не обязательно заводить отдельный идентификатор (и пароль) для каждого ресурса, достаточно зарегистрироваться на одном (любом) провайдере OpenID. (И эти провайдеры вполне себе функционируют и ныне, хотя потихонечку отмирают.) А затем использовать этот идентификатор OpenID (который представляет собой просто URL) для аутентификации на любом ресурсе, который эту самую аутентификацию через OpenID поддерживает.

Но мы же движемся к семантическому вебу, к интернету для роботов, сами того не осознавая. И логиниться под одним паролем всюду оказалось мало. Оказалось, нужно, чтобы один веб сервис (т.е. робот) мог обращаться к другому веб сервису (т.е. его API) от имени пользователя. Ну, чтобы какой-нибудь бот мог читать ваши твиты, не давать же ему пароль от Твиттера? Для решения этой задачи придумали OAuth.

OAuth называют протоколом авторизации. Это не проверка идентичности пользователя, т.е. аутентификация. Это предоставление доступа (от имени пользователя), т.е. авторизация.

На самом деле OAuth — целых две штуки. Версия 1.0 оказалась настолько сложной, что буквально единицы сервисов (например, Twitter) его успели внедрить до появления OAuth 2.0. Версию 2.0 специально упростили. Но в результате не специфицировали многие моменты поведения. Пытались это дело устаканить, махнули на всё рукой, и «протокол» переименовали во «фреймворк».

OAuth 2.0 logo

Более менее формальное описание OAuth 2.0 изложено в RFC 6749 (2012 год, за авторством Майкрософта). Это документик о двенадцати главах, трёх приложениях и семидесяти шести страницах. Но его прочтение (по слухам) не поможет вам создать своего OAuth провайдера. А передо мной возникла такая задача. Но одни правильные ребята написали правильную книжку, где всего на сорока пяти веб страничках изложены все основы OAuth 2.0, с объяснениями и примерами кода. Для создания OAuth клиента для любого OAuth провайдера достаточно будет прочитать первые пять страниц.

Итак. У нас есть юзер, т.е. человек, который знает пароль и владеет некоторыми ресурсами на некотором сервисе. У нас есть этот сервис, где есть твиты, картинки, документы и прочие ресурсы, принадлежащие этому человеку. У нас есть другой сервис, клиент OAuth, который хочет получить доступ к этим самым ресурсам этого человека. И у нас есть сервер авторизации OAuth, который может дать права клиенту на ресурсы. Сервер авторизации и сервер ресурсов, понятное дело, должны как-то взаимодействовать (как именно, OAuth не волнует), поэтому часто это вообще один сервер.

OAuth 2.0 roles

Допустим, мы создали новую, милую и хорошую веб службу. И этой службе нужно получить доступ, например, к профилю пользователя на Google. Собственно, получение доступа к профилю пользователя на каком-то солидном и популярном ресурсе или социальной сети, чтобы узнать его имя, и, допустим, email, вполне достаточно для аутентификации пользователя на вашем маленьком сервисе. Это и называется «войти через Google/Facebook/VKontakte/etc».

Для начала нам нужно нашу службу зарегистрировать на сервере авторизации. Везде это делается по-разному. Чаще всего это всякие девелоперские настройки вашего профиля, список авторизованных OAuth приложений, и тому подобное. Вы должны указать название и описание вашего сервиса, вероятно, некоторую иконку, а также очень важный параметр: Redirect URI. Это URI/URL на вашем сервисе, куда, в виде колбэка, будет приходить важная информация от сервера авторизации. Таких URI, как правило, можно указать несколько.

Взамен вам, для вашего сервиса, выдадут Client ID и Client Secret. Это просто две длинные строки, которые нужно будет вписать куда-то в настройки вашего приложения. Client Secret — это аналог пароля, поэтому хранить его нужно соответствующе, по возможности шифруя.

От сервера авторизации вам нужно знать ещё два URL, два метода его API: URL авторизации и URL получения токена. Они обычно заканчиваются на что-нибудь вроде oauth/authorize и oauth/token.

Итак, где-то у вас на сайте есть кнопочки вроде «Log in with Google» или «Connect to Google Drive». Когда пользователь их нажимает, начинается процедура авторизации. Ваш сервис перенаправляет браузер пользователя на oauth/authorize сервера авторизации. При этом в URL добавляются параметры.

  • client_id — тот самый идентификатор клиента, что вам выдали при регистрации вашего сервиса.
  • response_type — ожидаемый тип ответа от сервера авторизации, для данного случая должен быть «code». Хотя некоторые серверы авторизации не требуют этого параметра, ибо поддерживают только один вариант авторизации.
  • redirect_uri — один из тех адресов возврата, что вы указывали при регистрации.
  • scope — описание того, какие права вы хотите получить для доступа к ресурсам. Только читать твиты или ещё их и постить? Формат этого параметра определяется исключительно сервером авторизации (и сервером ресурсов). Обычно это какой-то набор строк (или URL), разделённых пробелами, по отдельной специальной строке для каждого разрешения. Гранулярность разрешений тоже зависит от сервера ресурсов. Например, у Google получить просто имя пользователя — это «profile», а узнать его емейлы — это дополнительно «email».
  • state — некоторое «состояние» вашего сервиса, эта строка просто вернётся вам без изменения в конце, можно использовать, чтобы отличить один ответ от сервера авторизации от другого. Точнее даже нужно использовать, для защиты от CSRF атак.

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

Кроме того, будут явно показаны те разрешения (scope), что запросило ваше приложение. На особо продвинутых серверах авторизации пользователь даже может снять некоторые галочки, и в результате разрешить несколько меньше, чем ваше приложение запрашивает.

Пользователь подумает-подумает, да и нажмёт на кнопку «Allow». Это — успех. Сервер авторизации, запомнив решение пользователя, редиректнет браузер на тот самый redirect URI. Здесь придут два параметра.

  • code — временный код для получения токена доступа. Некая произвольная строка. Срок действия этого кода — не больше нескольких минут.
  • state — тот state, который вы посылали ранее, без изменений.

Ну вы же запрашивали «response_type=code», и получили «code».

Если что-то пошло не так, то либо ошибка будет показана где-то на страницах сервера авторизации, и никакого редиректа не будет. Либо в вашем редиректе появятся параметры error, error_description или даже error_uri, показывающие код, описание или, соответственно, адрес страницы с описанием ошибки.

Мы получили code. Ваш сервер не должен этот код нигде хранить, а должен немедленно обменять его на токен. Для этого нужно сделать запрос на второй URL сервера авторизации. Нужно сделать POST на oauth/token. Сделать не из браузера, а из бэкенда вашего приложения. И передать другие параметры в теле запроса, в виде x-www-form-urlencoded данных.

  • grant_type — способ получения токена, в данном случае по коду, поэтому значение этого параметра должно быть «authorization_code». Некоторые серверы авторизации не используют этот параметр, потому что поддерживают только один вариант авторизации.
  • code — тот самый код, который вы получили от сервера авторизации шагом ранее.
  • redirect_uri — снова redirect uri, чтобы лишний раз подтвердить, что вы — это вы. Нужен ли он, зависит от сервера авторизации.

Клиент, т.е. ваш сервер, должен аутентифицировать себя для получения токена. Чаще всего для этого к запросу добавляются ещё два параметра.

  • client_id — снова ваш идентификатор клиента.
  • client_secret — тот самый секрет, который вы получили при регистрации, наконец-то он пригодился.

Или же идентификатор клиента и секрет выступают в роли имени пользователя и пароля и передаются в HTTP заголовке «Authorization», согласно Basic Auth. Или же достаточно обычного GET запроса, а все параметры передаются в виде обычного query. Тут все нюансы снова определяются сервером авторизации.

В ответ за запрос токена сервер авторизации возвращает нам (наконец-то) обыкновенный JSON. Этот JSON не содержит вложенных объектов, а только свойства в объекте верхнего уровня. Эти свойства нам и нужны.

  • access_token — тот самый токен доступа, ради которого всё это затевалось.
  • token_type — некая подсказка, как этим токеном пользоваться. Часто просто строка «bearer».
  • expires_in — сервер авторизации может указать время жизни выданного токена.
  • refresh_token — сервер авторизации может выдать дополнительный токен, который может использоваться для обновления основного токена без прохождения всей этой длинной процедуры авторизации. Это имеет смысл из соображений безопасности, чтобы основной токен менялся почаще, но при этом доступ был подольше.
  • scope — если пользователь снимал галочки и урезал доступ вашему приложению, тут может вернуться актуальный scope, который отличается от того, что вы запрашивали.
  • state — state может вернуться и здесь.

Если что-то пошло не так, то в JSONе ответа снова будут поля error, error_description или даже error_uri.

Ну вот почти и всё. Теперь у вас есть access_token. Он даёт право вашему приложению делать какие-то действия, ограниченные scope, от имени данного авторизованного пользователя на сервере ресурсов. Нужно только в каждом запросе на API сервера включать этот токен. В простейшем случае токен добавляется к запросу как параметр с именем «access_token» или «token». Или же нужно посылать заголовок «Authorization» типа «Bearer».

Authorization: Bearer <access_token_here>

Тут нюансы снова зависят от сервера авторизации и конкретного сервера ресурсов. Потому OAuth и назвали «фреймворком», на «протокол» не тянет, ибо слишком многие детали отданы на откуп конкретным реализациям. А от этих деталей очень сильно может зависеть секурность протокола вообще, именно поэтому OAuth 2.0 справедливо критикуют за дырявость.

Даже в рамках одного OAuth параметры авторизации могут передаваться в query части URL, в теле запроса, включая x-www-form-urlencoded и JSON (хорошо, что без XML обошлись), или в заголовках. Но, поверьте мне, OAuth всё же не выиграет приз в номинации «Наибольшее разнообразие способов передачи параметров в HTTP в рамках одной спецификации».

OAuth with auth code workflow

В рассмотренном выше наш сервер должен держать где-нибудь в конфигах два параметра: Client ID и Client Secret. Иногда это считается небезопасным, когда код не является доверенным, потому что пользователь (потенциально) имеет к нему (коду) полный доступ и может (потенциально) модифицировать его по своему желанию. Это касается приложений, выполняющихся в браузере (т.е. на JavaScript), и мобильных приложений (т.е. под iOS и Android, например).

Для такого случая Client Secret не выдаётся (не нужен), а получение токена происходит посредством так называемого implicit grant.

В первом запросе, на oauth/authorize, в параметре response_type указывается другое значение: «token». Т.е. мы говорим серверу авторизации, что не хотим никаких промежуточных кодов, а хотим сразу токен.

И сервер авторизации редиректит назад с двумя параметрами: token и state. Т.е. мы сразу получаем наш вожделенный токен. Но вот передаётся этот токен не в query, а во fragment.

https://my.cool.app/oauth/callback#token=Yzk5ZDczMzRlNDEwY&state=TY2OTZhZGFk

Фрагмент не передаётся на сервер, но доступен из JavaScript. А мы вроде как и не хотели его никому, кроме как коду на JavaScript, давать.

С мобильными приложениями всё ещё интереснее. Страницы сервера авторизации, с логином/паролем и кнопкой «Allow» отображаются в системном браузере или WebView. Куда происходит редирект? Вообще-то, куда угодно.

Сервера у нас нет, поэтому редиректить надо на приложение. В Android приложение можно навесить на открытие любого URL. В iOS нужно регистрировать для приложения свою схему URI, и редирект получается на нечто вроде «mycoolapp://oauth/callback#token=xxx&state=yyy».

В принципе, если мобильное приложение контролирует WebView, можно вообще никуда не редиректить, а просто отловить ответ 302 Found от сервера авторизации и взять заголовок «Location», т.е. адрес, куда должен произойти редирект.

В любом случае мобильное приложение получает доступ к URI редиректа, и получает оттуда токен. Ну а потом использует этот токен как обычно, добавляя в запросы к API сервера ресурсов.

Получается, что Redirect URI — это действительно URI, а не просто URL. Сервер авторизации особо к его содержимому не придирается. Главное, чтобы этот URI, полученный от клиента, в точности совпадал с URI, указанным клиентом при регистрации.

Поэтому, кстати, вполне можно тестировать OAuth локально, указав в Redirect URI что-нибудь вроде «http://localhost:12345/test/callback».

OAuth implicit grant workflow

Если вы успешно переварили всё то, что тут было написано, вы сможете разобраться с этим скриптом. И поймёте, зачем там запускать браузер. Это вполне реальный пример мелкой автоматизации манипуляций через Slack API с использованием OAuth 2.0. Удачи.

Ещё раз, хотите сделать свой OAuth 2.0 руками — сначала прочитайте OAuth.com.