Интеграция входа через внешний поставщик начинается с уверенности, что это одна задача, и заканчивается тремя неделями чтения документации конкретного сервиса. Первый сюрприз поджидает ещё до кода: популярный фреймворк авторизации не проверяет личность пользователя вовсе. Он выдает право доступа к чужим сведениям от имени владельца, и лишь следующий уровень стандарта добавляет к нему подтверждение того, кто именно стоит за выдачей. Второй сюрприз живёт в деталях: у каждого крупного поставщика свои названия областей, свои правила жизни токенов и свои особенности проверки подписей. Ниже разбор обеих ступеней, типичных ловушек интеграции и набора правил, который экономит недели.
Две разные задачи в паре протоколов и почему их путают
Основа пары это рамка делегированного доступа, опубликованная как стандарт в 2012 году. Её задача дать приложению право действовать от имени владельца сведений без получения его пароля: приложение получает ограниченный пропуск к конкретному набору функций, а владелец видит и подтверждает эту выдачу. Пропуск называется маркером доступа, и его судьба целиком на совести выдающей стороны: он может быть читаемой структурой, а может быть случайной строкой, значимой только для поставщика.
Путаница возникла из-за того, что выдача пропуска внешне похожа на вход пользователя. Поставщики начали опираться на рамку для организации входа, но без строгой спецификации каждая реализация решала вопрос по-своему, и результатом стал набор несовместимых схем. Ответом стал отдельный слой аутентификации поверх рамки, опубликованный в 2014 году: он добавляет специальный маркер личности в виде подписанной структуры с утверждениями о пользователе, сроком и адресате.
Различие формулируется просто: рамка отвечает на вопрос "какому приложению разрешено что-то делать", слой личности отвечает на вопрос "кто этот пользователь". Вход через внешний поставщик требует второго и опирается на первый. Смешение уровней приводит к ошибкам безопасности, которые описаны в официальных документах отдельно, и к наивным интеграциям, принимающим маркер доступа за подтверждение личности. Живой пример путаницы: приложение получает пропуск на чтение календаря пользователя и считает пользователя вошедшим, хотя сам пропуск не отвечает ни на один вопрос о человеке, ни на один вопрос о том, кто и как проходил проверку.
Код авторизации с защитой от перехвата как основной поток
Рамка описывает несколько способов получения пропуска, и половина из них сегодня не рекомендуется к использованию. Способ с выдачей пропуска прямо в адресной строке, созданный для клиентов без сервера, устарел: пропуск уезжает в журнал браузера и остаётся в истории. Способ с паролем владельца внутри приложения устарел ещё жёстче, он учит прикладывать пароль туда, где пароль не нужен. Рабочим стандартом остаётся способ с кодом авторизации: пользователь подтверждает выдачу на стороне поставщика, приложение получает одноразовый код в адресной строке, обменивает его на пропуск через защищённый канал и никогда не видит сам факт выдачи в открытом виде.
Код сам по себе уязвим к перехвату на устройстве без сервера, например в мобильном приложении, которое принимает ответ через локальную страницу. Лекарство, дополнительная пара случайных значений: приложение заранее генерирует проверочное значение и отправляет поставщику его свёртку, а в обмене кодом на пропуск предъявляет исходное значение. Поставщик сверяет свёртку с исходным и отклоняет обмен, если код перехвачен. Приём опубликован отдельным документом в 2015 году и стал обязательным для публичных клиентов в свежей редакции рамки.
Механика пары значений прозаична и сводится к двум командам:
verifier=$(openssl rand -base64 48 | tr -d '=+/' | head -c 64)
challenge=$(printf '%s' "$verifier" | openssl dgst -sha256 -binary | base64 | tr -d '=+/')
Свёртка уезжает в первый запрос на подтверждение, исходное значение остаётся в памяти приложения и предъявляется только при обмене кода. Угнать пару с середины пути невозможно: свёртка без исходного значения бесполезна для обмена.
Параметры на пути кода тоже являются защитой. Случайное значение состояния привязывает ответ поставщика к исходному запросу и отсекает подделку обратного вызова, а в слое личности добавляется одноразовое значение, которое связывает маркер личности с конкретной сессией входа и отсекает повторное использование. Пропуск любого из этих значений не виден в счастливом сценарии и всплывает только при разборе инцидента.
Провайдеры расходятся в деталях при общем стандарте
Стандарт описывает каркас, а конкретные поставщики заполняют его по-разному, и именно здесь интеграция съедает недели. Названия областей доступа не совпадают: у одного поставщика адрес электронной почты запрашивается одним словом, у другого составным, а третий требует отдельного подтверждения для дополнительных сведений. Утверждения в маркере личности носят разные имена для одинаковых сущностей, а необязательные поля у одного поставщика присутствуют всегда, у другого появляются по запросу.
Формат маркера доступа тоже расходится. Один поставщик выдаёт подписанную структуру, которую можно проверить локально, другой выдаёт случайную строку, и единственный способ узнать её свойства это обращение к специальному адресу проверки. Писать код под первый формат и надеяться на второй нельзя, надёжный путь это работа с маркером через официальный интерфейс поставщика либо ветвление по типу маркера.
Адреса узлов выдачи и проверки тоже стоит не вписывать руками, а читать из документа описания, который каждый совместимый поставщик публикует по фиксированному адресу. Такой документ перечисляет все узлы, поддерживаемые алгоритмы подписи и области. Чтение описания на старте интеграции спасает от ситуации, когда поставщик меняет адрес узла, и у всех интеграций с вписанными адресами вход ломается в один день.
Документ описания небольшой, и его стоит показать целиком, чтобы видеть состав:
/.well-known/openid-configuration
authorization_endpoint адрес подтверждения выдачи
token_endpoint адрес обмена кода на маркеры
jwks_uri адрес набора ключей проверки
issuer ожидаемое значение издателя
supported_scopes перечень областей доступа
Проверка маркера личности на своей стороне
Маркер личности подписан, и проверка подписи это не рекомендация, а обязательное требование. Ключи проверки публикуются поставщиком в виде набора по отдельному адресу, набор периодически ротируется, и код проверки обязан подхватывать новый ключ без выпуска новой версии приложения. Частая ошибка самодельных интеграций, кэш ключей без обновления: после ротации все проверки падают, и вход в систему не работает у всех пользователей разом.
Проверка содержимого не менее важна, чем подпись. Подтверждение адресата должно совпадать с идентификатором приложения, иначе маркер, выпущенный для чужого приложения, будет принят как свой. Подтверждение издателя должно совпадать с ожидаемым поставщиком. Одноразовое значение из запроса входа сверяется с маркером, отсекая повторное использование. Срок проверяется с небольшим допуском на расхождение часов. Каждый из этих шагов требует отдельной строки кода, и пропуск любого из них не виден в счастливом сценарии.
Отдельная ловушка, разные маркеры для разных целей. Каждый ключ в наборе проверки носит собственный идентификатор, и в заголовке маркера указан идентификатор того ключа, которым он подписан. Проверка выбирает ключ из набора по этому идентификатору, не перебирая все подряд, и при ротации старый ключ остаётся в наборе до конца жизни последних маркеров. Маркер личности не является маркером доступа, и попытка использовать один вместо другого заканчивается отказом на стороне ресурса. Обратная попытка, принимать маркер доступа как подтверждение личности без обращения к адресу проверки, приводит к принятию маркеров, выданных вообще другим клиентам, потому что у рамки нет требования о читаемости получателя в маркере доступа.
Кэш служебных узлов обязан иметь срок жизни, и это компромисс: жёсткое кэширование без обновления ломает вход после ротации ключей поставщика, а отсутствие кэша превращает каждый вход в зависимость от доступности постороннего узла. Рабочая пропорция, короткий срок с обновлением в фоне и повторным использованием последнего удачного описания при недоступности источника.
Интеграционные проверки против живых песочниц поставщика
Единственный надёжный способ убедиться в совместимости, прогнать интеграцию через настоящую песочницу поставщика. Спецификация описывает каркас, а поведение на границах, правила отзыва, точный состав утверждений, поведение при повторном использовании обновляющего маркера, у каждого поставщика своё. Интеграционный набор проверяет именно эти места: полный цикл входа, отказ при подделке состояния, обновление по обновляющему маркеру, поведение при просроченном маркере, отсутствие лишних прав в выданном пропуске.
Набор стоит держать в сборке и запускать регулярно, а не при разовом внедрении. Поставщики меняют поведение и сроки жизни, и интеграционная проверка, прогнанная год назад, не защищает от тихого изменения. Отдельный плюс регулярного прогона, раннее обнаружение совпадений: если поставщик отзывает обновляющие маркеры по своей политике, интеграционный набор покажет это до пользователей.
Запуск интеграций требует изоляции от случайных срабатываний: учётная запись песочницы, отдельный идентификатор приложения и аккуратное обращение с секретами. Секреты приложения в коде репозитория это отдельная классическая ошибка интеграций, и решение, хранилище секретов с выдачей на этапе сборки, должно появиться до первого объединённого запроса, а не после первой утечки.
В интеграционном наборе стоит проверять и отказные ветви, которые в счастливом сценарии не видны: подделка значения состояния отклоняется, маркер с чужим адресатом отбрасывается, просроченный маркер считается отсутствующим, повторное использование одноразового значения пресекается. Отказные ветви проверяют чаще, чем удачные, потому что именно они ломаются при обновлениях поставщика, а их молчаливое исчезновение не замечает никто до разбора инцидента.
Правила интеграции которые экономят недели
Практика интеграций сводится к короткому перечню, который удобно держать перед глазами:
- вход всегда через слой личности, а не через прямую работу с рамкой доступа, маркер доступа не считается подтверждением личности;
- основной поток, код авторизации с проверочным значением, без исключений для веб-приложений с серверной частью;
- адреса узлов читаются из документа описания поставщика, вписывание адресов руками запрещено регламентом;
- проверка маркера личности включает подпись, адресата, издателя, срок и одноразовое значение из запроса входа;
- ключи проверки обновляются по расписанию из набора поставщика, а не вшиваются в приложение;
- секреты приложения живут в хранилище секретов и выдаются на этапе сборки и запуска.
К перечню стоит добавить правило тестирования: интеграционный набор против песочницы запускается регулярно, и его зелёный статус условие выпуска. Всё перечисленное скучное, и именно поэтому работает: недели, потраченные на первый вход по этим правилам, возвращаются при каждом следующем поставщике.
Вывод про две ступени и честные границы спецификации
Рамка доступа и слой личности решают разные задачи, и путаница между ними стоит дороже любой другой ошибки интеграции. Основа описывает выдачу ограниченного пропуска, слой поверх неё описывает подтверждение личности, и рабочая интеграция использует обе ступени по назначению: вход через маркер личности, доступ к сведениям через маркер доступа.
Конкретные поставщики добавляют поверх общего стандарта свои особенности, и противостоять им нужно не надеждой на строгую совместимость, а документом описания, аккуратной проверкой маркеров и регулярным интеграционным набором против песочницы. Тогда различия провайдеров превращаются из источника сюрпризов в список известных отличий, а вход через внешний поставщик становится рядовой частью системы, а не постоянным источником ночных разборов.