OAuth — это стандартный протокол. Ведь так? И для OAuth 2.0 есть клиентские библиотеки практически на всех языках программирования, которые можно представить.
Вероятно, вы подумаете, что имея клиентскую библиотеку, можно реализовать OAuth для любого API буквально за десять минут. Или хотя бы за час.
Если вам это удастся, то, пожалуйста, сообщите об этом нам — мы угостим вас изысканным ужином и послушаем, как у вас это получилось.
▍ OAuth на практике
Мы реализовали OAuth для пятидесяти самых популярных API, например, Google (Gmail, Calendar, Sheets и так далее), HubSpot, Shopify, Salesforce, Stripe, Jira, Slack, Microsoft (Azure, Outlook, OneDrive), LinkedIn, Facebook и для других OAuth API.
Наш вывод: в реальном мире опыт работы с OAuth сравним с опытом работы с браузерными JavaScript API в 2008 году. Существует общий консенсус о различных аспектах, но в реальности у каждого API есть собственная интерпретация стандарта, особенности реализации, нестандартные поведения и расширения. В результате за каждым углом вас ожидают выстрелы в ногу.
Если бы это так не раздражало, то было бы довольно забавным. Но давайте рассмотрим конкретные примеры!
Проблема 1: стандарт OAuth слишком большой и сложный
«Этот API тоже использует OAuth 2.0, и мы создали его несколько недель назад. Наверно, до завтра я справлюсь» – знаменитые слова стажёра
OAuth — очень большой стандарт. На официальном сайте OAuth 2.0 сейчас указано 17 разных RFC (определяющих стандарт документов), вместе описывающих принцип работы OAuth 2. В них раскрывается всё, от фреймворка OAuth и токенов Bearer до моделей угроз и JWT приватных ключей.
«Но ведь не все эти RFC важны для простой авторизации по токенам при помощи API?», — спросите вы.
Да, вы правы. Давайте сосредоточимся только на том, что релевантно для типичного примера доступа третьей стороне при помощи API:
- Стандарт OAuth: по умолчанию сейчас используется OAuth 2.0, но некоторые по-прежнему используют OAuth 1.0a (а на подходе уже 2.1). Разобравшись, какую версию использует ваш API, переходите к:
- Типу Grant: вам нужны «authorization_code», «client_credentials» или «device_code»? Что они делают и когда необходимо применять каждый из них? В случае сомнений попробуйте «authorization_code».
- Примечание: токены обновления — это тоже grant, но довольно особенный. Способ их работы стандартизирован, однако способ их запроса — нет. Подробнее об этом ниже.
- Теперь, когда вы готовы к запросам, посмотрите, сколько (72, если быть точным) существует официальных параметров OAuth с конкретным значением и поведением. Самые частые примеры — это «prompt», «scope», «audience», «resource», «assertion» и «login_hint». Однако, по нашему опыту, большинство поставщиков API, похоже, не знает об этом списке, как, наверно, и вы до сего момента, поэтому не стоит особо об этом волноваться.
Если вы думаете, что это всё равно слишком сложно и требует долгого изучения, то мы склонны с вами согласиться.
И большинство команд, разрабатывающих публичные API, наверно, тоже с этим согласно. Вместо реализации полного подмножества OAuth 2.0 они просто реализуют части OAuth, которые, по их мнению, будут необходимы в сценарии использования их API. Это приводит к созданию длинных страниц с описаниями того, как работает OAuth в конкретном API. Но мы вряд ли можем их винить; они руководствовались только самыми лучшими побуждениями. А если бы они попытались реализовать стандарт целиком, то документация представляла бы собой небольшую книгу!
Поток authorization_code OAuth в Salesforce. Что может не понравиться в этом чётком и наглядном объяснении процесса из десяти этапов?
Проблема в том, что каждый имеет слегка отличающееся понимание того, какое подмножество OAuth релевантно для него, поэтому в результате получается множество разных (под-)реализаций.
Проблема 2: каждый реализует OAuth немного по-своему
Так как каждый API реализует собственное подмножество OAuth, мы быстро попадаем в ситуацию, когда приходится внимательно читать длинные страницы документации по OAuth:
- Какие параметры они требуют в вызове авторизации?
- В случае Jira, параметр «audience» — это ключ (которому должно быть присвоено конкретное фиксированное значение). Google предпочитает обрабатывать это через разные scope, но ему очень важен параметр «prompt». Тем временем, кто-то в Microsoft обнаружил параметр «response_mode», который всегда должен иметь значение «query».
- Notion API подходит к этому радикально: повсюду использует параметр «scope». На самом деле в документации по этому API нет упоминаний слова «scope». В Notion они называются «capabilities» и задаются при регистрации приложения. Нам понадобилось полчаса, чтобы в этом разобраться. Зачем разработчики решили заново изобрести велосипед?
- С «offline_access» ситуация ещё хуже: сегодня в большинстве API срок действия токенов доступа истекает очень быстро. Чтобы получить токен обновления, нужно затребовать «offline_access», что необходимо выполнить через параметр, scope, и то, что задаётся при регистрации приложения OAuth. Подробности нужно узнавать у специалиста по API или OAuth.
- Что требуется указать в вызове запроса токена?
- Некоторые API, например, Fitbit, настаивают на получении данных в заголовках. Большинству необходимо, чтобы они были в теле, закодированные как «x-www-url-form-encoded», а некоторые исключения наподобие Notion предпочитают получать их в JSON.
- Некоторые хотят аутентифицировать этот запрос при помощи Basic auth. Многие этим не заморачиваются. Но будьте внимательны, уже завтра они могут всё поменять.
- Куда перенаправлять моих пользователей для авторизации?
- У Shopify и Zendesk есть модель, в которой каждый пользователь получает поддомен вида {subdomain}.myshopify.com. И да, это относится и к странице авторизации OAuth, поэтому лучше встроить динамические URL в модель и код фронтенда.
- У Zoho Books есть разные дата-центры для клиентов в разных странах. Они должны помнить, где находятся их данные: для авторизации приложения клиенты из США должны перейти на
https://accounts.zoho.com
, европейцы — наhttps://accounts.zoho.eu
, а индийцы — наhttps://accounts.zoho.in
. И так далее.
- Но, по крайней мере, я могу выбирать свой URL обратного вызова, ведь так?
- Если выбрать в качестве обратного вызова «localhost:3003/callback» для Slack API, он любезно напомнит вам «для безопасности использовать https». Да, даже для localhost. К счастью, существуют решения для перенаправления OAuth на localhost.
Этот список можно продолжать ещё долго, но надеюсь, смысл вы поняли.
OAuth слишком сложен; давайте создадим более простую версию OAuth, в которой есть всё, что нам нужно!
Проблема 3: многие API добавляют к OAuth нестандартные расширения
Даже несмотря на обширность стандарта OAuth, многие API, похоже, всё равно находят в нём отсутствие нужных им функций. Часто мы встречаемся с проблемой необходимости для работы с API дополнительных данных наряду с «access_token». Разве не было бы здорово, если бы эти дополнительные данные могли возвращаться вместе с access_token в потоке OAuth?
Мы и в самом деле считаем это хорошей идеей; или, по крайней мере, она получше, чем заставлять пользователей потом выполнять изощрённые запросы к API для получения этой информации (да, мы о тебе, Jira). Но это означает, что необходимо более нестандартное поведение, которое специально требуется реализовывать для каждого API.
Вот небольшой список нестандартных расширений, которые нам встречались:
- Quickbooks использует «realmID», который нужно передавать с каждым запросом к API. Единственный раз, когда он сообщает этот «realmID» — это дополнительный параметр в обратном вызове OAuth. Лучше хранить его где-то в безопасном месте!
- Braintree поступает так же с «companyID»
- Salesforce использует отдельный базовый URL API для каждого клиента; они называются «instance_url». К счастью, он возвращает «instance_url» пользователя вместе с токеном доступа в ответе токена, но его всё равно нужно спарсить оттуда и сохранить.
- К сожалению, Salesforce делает ещё более раздражающие вещи: срок действия токенов доступа истекает после заранее заданного периода времени, который может настраивать пользователь. Пока всё здорово, но по какой-то причине он не сообщает в ответе токена, когда истечёт срок только что полученного доступа (все остальные это делают). Вместо этого нужно запрашивать дополнительную конечную точку подробностей о токенах, чтобы получить текущую дату истечения токена. Почему, Salesforce, почему?
- В Slack есть два типа scope: scope, которые вы имеете как бот Slack, и scope, которые позволяют выполнять действия от лица пользователя, авторизовавшего приложение. Это умно, но вместо добавления разных scope для каждого они случая разработчики реализовали отдельный параметр «user_scopes», который нужно передавать в вызове авторизации. Об этом лучше знать, а поддержку этой особенности в вашей библиотеке OAuth найти вряд ли получится.
Ради краткости и простоты мы опустим множество не очень стандартных потоков OAuth, с которыми нам приходилось сталкиваться.
Проблема 4: «invalid_request» — отлаживать потоки OAuth сложно
Отлаживать распределённые системы всегда сложно. Ещё сложнее это делать, когда сервис, с которым ты работаешь, использует неопределённые и обобщённые сообщения об ошибках.
У OAuth2 есть стандартизованные сообщения об ошибках, но они не более информативны, чем пример из заголовка (который, кстати, является одним из рекомендованных сообщений об ошибках из стандарта OAuth).
Возможно, вы заявите, что OAuth — это стандарт, а для каждого API есть документация, так что же тут отлаживать?
Многое. Не могу передать, насколько часто в документации присутствуют ошибки. Или отсутствуют подробности. Или она не обновлялась с последним изменением. Или вы что-то упустили при первом её изучении. Добрые 80% реализуемых нами потоков OAuth имеют проблемы в первой реализации и требуют отладки.
Некоторые потоки разваливаются, казалось бы, по случайным причинам: например, LinkedIn OAuth разваливается при передаче параметров PKCE. Какую же ошибку мы получаем? «client error — invalid OAuth request». И о чём же она нам говорит? Нам потребовался час, чтобы понять, что поток ломается из-за параметров PKCE (опциональных, которые обычно игнорируют).
Ещё одна распространённая ошибка заключается в передаче scope, не совпадающих с теми, которые вы предварительно зарегистрировали с приложением. (Предварительно регистрируемые scope? Да, сегодня многие API требуют их.) Часто это приводит к получению обобщённого сообщения о проблеме с scope. Да уж.
Проблема 5: неудобство согласования при разработке поверх API
Если вы надстраиваете чужую систему, используя её API, то вы, вероятно, находитесь в неудобном положении. Клиенты просят реализовать интеграцию, потому что они уже пользуются другой системой. И вам нужно удовлетворить их потребности.
Если честно, многие API достаточно либеральны и предоставляют простые самообслуживающиеся потоки регистрации, позволяющие разработчикам регистрировать свои приложения и использовать OAuth. Однако некоторые из самых популярных API требуют проверки, прежде чем приложение станет публичным и с ним можно будет работать пользователям. Повторюсь, ради справедливости нужно сказать, что большинство процессов проверки вполне разумны и их выполнение требует всего нескольких дней. В целом, с точки зрения качества и безопасности конечных пользователей они приносят пользу.
Однако в некоторых печальных случаях проверка затягивается на месяцы и даже требует подписания договоров об участии в прибыли:
- Если вам нужен scope доступа к уязвимым пользовательским данным, например, к содержимому электронной почты, Google требует «проверки безопасности». Мы слышали, что для прохождения таких проверок нужны дни или даже недели, а также нетривиальный объём труда со стороны разработчиков.
- Хотите организовать интеграцию с Rippling? Готовьтесь ответить на тридцать с лишним вопросом и пройти скрининг безопасности препродакшена. Мы слышали, что для получения доступа требуются месяцы (если вас одобрили).
- HubSpot, Notion, Atlassian, Shopify, да и практически все остальные, у кого есть маркетплейсы интеграций или магазины приложений, требуют проверки для попадания в список. Некоторые проверки вполне умеренны, а другие требуют предоставления демонстрационных логинов, видеоинструкций, постов в блогах (да!) и многого другого. Однако попадание в маркетплейс или магазин часто необязательно.
- У Ramp, Brex, Twitter и многих других нет самообслуживаемого потока регистрации для разработчиков; они требуют, чтобы разработчики заполняли формы для доступа к руководствам. Многие обрабатывают запросы быстро, от других же нужно ждать ответа несколько недель.
- Xero — один из радикальных примеров монетизируемого API: если вы хотите преодолеть ограничение в 25 подключенных аккаунтов, то вам придётся стать партнёром Xero и зарегистрировать своё приложение в его магазине приложений. После этого он будет взимать долю в 15% от дохода с каждого лида, сгенерированного из этого магазина (информация актуальна на момент публикации статьи).
Проблема 6: безопасность OAuth — это сложная и движущаяся мишень
С обнаружением разных видов атак и эволюцией веб-технологий стандарт OAuth тоже менялся. Если вы стремитесь реализовать современные рекомендации по безопасности, то у рабочей группы OAuth есть для вас довольно длинное руководство. А если работаете с API, который и сегодня использует OAuth 1.0a, то поймёте, что обратная совместимость — это проблема, которую нужно решать постоянно.
К счастью, уровень защиты с каждой итерацией повышается, но часто за это приходится расплачиваться дополнительным трудом разработчиков. Грядущий стандарт OAuth 2.1 может сделать некоторые из современных рекомендаций обязательными и включает в себя обязательный PKCE (сегодня его требуют очень немногие API), а также дополнительные ограничения на токены обновления.
Самое важное заявленное изменение, вероятно, будет связано с истечением срока действия и с развитием токенов обновления. На поверхности процесс кажется простым: когда истекает срок действия токена доступа, мы обновляем его при помощи токена обновления и сохраняем новый токен доступа и токен обновления.
Однако в реальности, когда мы реализуем это, нужно учесть следующее:
- Условия гонки: как гарантировать, что при обновлении текущего токена доступа не будут выполняться другие запросы?
- Некоторые API завершают срок действия токена обновления, если его не используют определённое количество дней (или если пользователь аннулировал доступ). Стоит ожидать, что некоторые обновления не будут выполняться.
- Некоторые API передают новый токен обновления при каждом запросе обновления …
- … но некоторые молчаливо предполагают, что вы сохраните старый токен обновления и продолжите его использовать.
- Некоторые API сообщают срок истечения токена доступа в абсолютных значениях. Другие сообщают относительные «секунды от текущего момента». А в некоторых, например, в Salesforce, получить подобный вид информации не так уж просто.
И последнее: то, о чём мы пока не говорили
К сожалению, мы затронули только малую часть аспектов реализации OAuth. Запустив поток OAuth и начав получать токены доступа, мы должны ещё подумать о следующем:
- Как безопасно хранить эти токены доступа и токены обновления. Они похожи на пароли к аккаунтам пользователей. Но хэширование — это не вариант: вам нужно безопасное и обратимое шифрование.
- О проверке того, что предоставленные scope соответствуют запрошенным scope (некоторые API позволяют пользователям менять scope, предоставляемые в потоке авторизации).
- Как избегать условий гонки при обновлении токенов.
- О выявлении токенов доступа, аннулированных пользователем на стороне поставщика.
- Как сообщать пользователям об истечении срока действия токенов доступа, чтобы они при необходимости повторно авторизировали приложение.
- Как аннулировать токены доступа, которые вам больше не нужны (или когда пользователь запросил их удаление согласно GDPR).
- Об изменениях в scope OAuth, багах поставщиков, отсутствующей документации и так далее.
Пол-лимона подарков от RUVDS. Отвечай на вопросы и получай призы 🍋
Автор:
ru_vds