Регистрация на новом сайте требует придумать пароль, а потом его запомнить. Через неделю человек возвращается и не помнит, какой из трёх привычных вариантов он выбрал. Кнопка входа через уже знакомый сервис снимает эту возню одним нажатием, и за внешней простотой скрывается целый протокол делегирования доступа, который разрабатывался несколько лет и пережил не одну волну исправлений.
Появление OAuth связано с нехваткой стандарта для передачи прав доступа
История началась в 2006 году. Разработчик трудился над реализацией единого входа для одной из платформ, а владелец сервиса закладок искал способ разрешить сторонним мини-приложениям доступ к данным своих пользователей. На встрече выяснилась неприятная вещь: готового открытого стандарта для управления доступом не существовало.
Группа разработчиков собралась в апреле 2007 года, чтобы написать черновик открытого протокола. К работе подключились инженеры крупных компаний, а к июлю появился первый вариант спецификации. Третьего октября 2007 года была выпущена версия 1.0.
Дальше начался путь стандартизации. В ноябре 2008 года на очередной встрече организации по стандартизации интернета прошло обсуждение, и протокол решили принять в работу. В апреле 2010 года первая версия вышла отдельным документом, а с августа того же года все сторонние приложения одной известной платформы обязали использовать новый механизм.
Версия 2.0 строилась на опыте эксплуатации первой, но оказалась с ней несовместимой. Она получила номер RFC 6749 и вышла в октябре 2012 года. Документ, описывающий работу с токенами доступа, появился тогда же под номером RFC 6750. Оба относятся к категории документов, определяющих стандарты, а не рекомендаций.
Обновления продолжаются. Документ о применении протокола в мобильных приложениях, о защите от перехвата кода и о лучших практиках безопасности постепенно сводятся в единую версию 2.1, работа над ней ещё не завершена.
Протокол решает задачу делегирования, а не проверки личности
Главная путаница в этой теме связана с назначением технологии. Расшифровка слова как открытой авторизации неточна. Речь идёт о делегировании доступа: владелец ресурса разрешает стороннему приложению работать с частью своих данных, не передавая ему пароль.
Роли в протоколе распределены чётко. Владелец ресурса это человек. Клиент это приложение, которое запрашивает доступ. Сервер авторизации выдаёт разрешения и токены. Сервер ресурсов хранит защищённые данные и проверяет предъявленные токены.
Ключевой момент состоит в том, что пароль остаётся на сервере авторизации. Клиент его не видит. Он получает токен, то есть ограниченный по времени и объёму пропуск, который можно отозвать, не меняя пароль пользователя.
Именно поэтому протокол удобно использовать для входа. Получив подтверждение, что человек успешно прошёл проверку у поставщика учётных данных, приложение создаёт или находит свою учётную запись и открывает сессию. Для этого поверх OAuth пристроен отдельный слой, описывающий передачу сведений о личности и называемый OpenID Connect.
Авторизационный код это основной сценарий для веб-приложений
Стандарт описывает несколько способов получить разрешение. Для сайта с серверной частью применяется сценарий с кодом авторизации.
Последовательность выглядит так. Приложение перенаправляет браузер пользователя на сервер авторизации и указывает свой идентификатор, список запрашиваемых прав, адрес возврата и случайное значение для защиты от подделки. Пользователь видит экран согласия и подтверждает доступ. Сервер авторизации возвращает браузер на адрес возврата, добавив короткоживущий код.
Дальше браузерная часть заканчивается. Серверная часть приложения обменивает код на токены, предъявляя при этом свой секрет. Только после этого обмена выдаётся токен доступа, а при необходимости и токен обновления.
Такой порядок не случаен. Код летит через браузер, где его может увидеть кто угодно, но сам по себе он бесполезен без секрета клиента, который остаётся на сервере. Обмен выполняется по защищённому соединению и требует подлинности обеих сторон.
Пример запроса на получение разрешения:
https://accounts.example.com/authorize
?response_type=code
&client_id=web-portal
&redirect_uri=https%3A%2F%2Fportal.example.net%2Fcallback
&scope=openid%20profile%20email
&state=x7Kp2mQa91
Параметр состояния заслуживает отдельного внимания. Приложение генерирует случайную строку, запоминает её в сессии пользователя и сверяет при возврате. Если значения не совпали, ответ отбрасывается. Это защита от подделки запроса, при которой злоумышленник пытается подсунуть приложению свой код.
Обмен кода на токен выполняется уже серверным запросом:
curl -X POST https://accounts.example.com/token \
-d grant_type=authorization_code \
-d code=полученный_код \
-d redirect_uri=https://portal.example.net/callback \
-d client_id=web-portal \
-d client_secret=секрет_клиента
В ответ приходит набор полей с токеном доступа, сроком его действия и, если запрошено, токеном обновления.
Расширение для мобильных приложений защищает от перехвата кода
Публичные клиенты, к которым относятся одностраничные приложения и мобильные программы, не умеют хранить секрет. Любой секрет, попавший в собранный пакет или в сценарий страницы, перестаёт быть секретом.
Дополнительная мера решает эту проблему. Приложение при первом обращении формирует случайное значение и сохраняет его у себя, а в запрос на разрешение добавляет не само значение, а его производную, полученную необратимым преобразованием. При обмене кода приложение предъявляет исходное значение. Сервер проверяет, что производная совпадает, и только тогда выдаёт токены.
Смысл защиты прост. Перехваченный код нельзя обменять, не зная исходного значения, которое никогда не покидало устройство. Злоумышленнику достаётся бесполезная строка.
Вторая мера касается самого механизма возврата. Вместо перенаправления на произвольный адрес с параметрами приложение использует специальную схему или заявляет точный адрес возврата, который сервер сверяет посимвольно. Это закрывает путь для увода ответа на чужой ресурс.
Отдельный старый сценарий, при котором токен возвращался прямо в адресной строке, давно считается небезопасным и выводится из применения. Причина в том, что токен попадает в историю браузера, в журналы промежуточных узлов и в заголовок перехода на сторонние ресурсы.
Обмен данными между сервисами обходится без участия человека
Не все сценарии предполагают живого пользователя. Иногда одному сервису нужно обратиться к другому от своего имени: забрать статистику, отправить уведомление, синхронизировать справочник.
Для этого применяется сценарий с учётными данными клиента. Приложение предъявляет свой идентификатор и секрет напрямую серверу авторизации и получает токен, привязанный к самому приложению, а не к человеку.
Второй вариант касается случаев, когда сервис действует от имени конкретного пользователя, но в момент выдачи токена его нет за клавиатурой. Тогда используется предварительно сохранённый токен обновления. Приложение обменивает его на новый токен доступа, не беспокоя владельца.
Токен обновления это чувствительный артефакт. Он живёт долго и позволяет получать новые токены доступа, поэтому хранить его нужно в защищённом хранилище, а не в открытой таблице. При компрометации его отзывают, и вся цепочка дальше обрывается.
Токены различаются по области действия, сроку и способу проверки
Разобравшись с получением, стоит разобраться с содержимым. Токен доступа предъявляется серверу ресурсов при каждом запросе, обычно в заголовке авторизации:
curl -H "Authorization: Bearer некоторый_токен" \
https://api.example.com/v1/profile
Область действия задаётся при запросе и ограничивает возможности токена. Если приложение просит доступ к профилю и почте, оно не сможет читать файлы пользователя. Это ограничение работает только при дисциплинированной проверке на стороне сервера ресурсов: недостаточно выдать узкий токен, нужно ещё отклонять запросы за пределами области.
Срок жизни токена доступа делают коротким. Часы, иногда минуты. Это снижает ущерб от утечки. Долгие сессии поддерживаются за счёт токена обновления, который выдаётся реже и хранится надёжнее.
Формат токена бывает разным. Прозрачная строка требует обращения к серверу авторизации при каждой проверке, зато позволяет отозвать её мгновенно. Самоценный токен, содержащий подписанные утверждения о пользователе, проверяется без сетевого обращения, но отзывается хуже: приходится либо ждать истечения срока, либо вести список отозванных значений.
Выбор между ними определяется требованиями. Если важна мгновенная реакция на отзыв, подходит прозрачная строка. Если важна нагрузка и скорость, а срок жизни короток, удобнее самоценный токен.
Безопасность интеграции держится на защите адреса возврата и проверке параметров
Протокол прошёл через несколько громких разборов уязвимостей, и каждый из них оставил после себя правило.
Первое правило касается адреса возврата. Сервер авторизации обязан сверять его с заранее зарегистрированным значением полностью, а не по началу строки. Проверка по префиксу позволяет подставить похожий домен и увести код на чужой ресурс. Этот класс ошибок описан в модели угроз и встречается в реальных проектах до сих пор.
Второе правило касается выбора сервера. Если приложение поддерживает нескольких поставщиков, злоумышленник может подменить ответ так, чтобы клиент отправил данные не тому адресату. Защита состоит в том, чтобы при каждом обращении явно указывать, к какому серверу идёт запрос, и проверять соответствие ответа ожиданиям.
Третье правило касается параметра состояния. Его генерация, сохранение и сверка обязательны. Без него злоумышленник может привязать чужую учётную запись к своей сессии.
Четвёртое правило связано с регистрацией адресов. Лишние адреса возврата в настройках приложения расширяют поверхность атаки. Их стоит удалять, а не оставлять на всякий случай.
Пятое правило касается расхождений при слиянии учётных записей. Если приложение связывает вход через сторонний сервис с уже существующим аккаунтом по адресу почты, нужно убедиться, что поставщик подтвердил владение этим адресом. Иначе кто угодно зарегистрируется под чужой почтой и получит доступ к чужому кабинету.
Практическая реализация требует внимания к хранению сессии и отзыву
Код обмена обычно короткий, а ошибки прячутся в деталях.
Первую деталь удобно разобрать на примере серверного обработчика на языке Python с использованием распространённой библиотеки:
import os, secrets
from flask import Flask, session, redirect, request
from authlib.integrations.flask_client import OAuth
app = Flask(__name__)
app.secret_key = os.environ["SESSION_SECRET"]
oauth = OAuth(app)
provider = oauth.register(
name="identity",
client_id=os.environ["CLIENT_ID"],
client_secret=os.environ["CLIENT_SECRET"],
server_metadata_url="https://accounts.example.com/.well-known/openid-configuration",
client_kwargs={"scope": "openid profile email"},
)
@app.route("/login")
def login():
state = secrets.token_urlsafe(24)
session["oauth_state"] = state
return provider.authorize_redirect(
redirect_uri="https://portal.example.net/callback", state=state
)
@app.route("/callback")
def callback():
if request.args.get("state") != session.pop("oauth_state", None):
return "Некорректный ответ поставщика", 400
token = provider.authorize_access_token()
userinfo = token.get("userinfo")
if userinfo is None:
userinfo = provider.userinfo(token=token)
return establish_session(userinfo)
Здесь видно сразу три обязательных шага: генерация состояния, его сверка при возврате и получение сведений о пользователе уже после обмена кода. Попытка доверять данным из адресной строки до обмена считается грубой ошибкой.
Вторую деталь стоит проговорить отдельно. После установления сессии приложение обычно больше не нуждается в исходном токене. Если доступ к сторонним данным не требуется, токен нужно забыть, а не складывать в базу на всякий случай. Меньше сохранённых секретов означает меньший ущерб при взломе.
Третья деталь касается выхода из системы. Локальный выход завершает сессию приложения, но не завершает сессию у поставщика учётных данных. Пользователь, который ожидает полного выхода, может быть удивлён, обнаружив, что повторное нажатие кнопки снова пускает его внутрь без пароля. Честный интерфейс либо предлагает отдельное действие для выхода у поставщика, либо прямо сообщает о разнице.
Четвёртая деталь это обработка отзыва. Пользователь имеет право лишить приложение доступа у себя в настройках. Приложение обязано корректно пережить ситуацию, когда токен обновления больше не работает: показать понятное сообщение и предложить войти заново, а не падать с невнятной ошибкой.
Польза и риски такого входа видны на длинной дистанции
У интеграции есть очевидные плюсы. Скорость регистрации растёт, потому что исчезает форма с паролем и подтверждением почты. Нагрузка на поддержку падает, ведь восстановление забытого пароля больше не требуется. Безопасность учётных данных повышается, поскольку пароль не хранится в приложении и не может утечь вместе с его базой.
Есть и обратная сторона. Человек попадает в зависимость от поставщика: если сервис входа недоступен, войти не получится. Приложение получает сведения о пользователе, объём которых определяется запрошенными правами, и злоупотребление этим доверием бьёт по репутации. Наконец, часть пользователей принципиально не желает связывать учётные записи между сервисами.
Отсюда практический вывод. Вход через сторонний сервис стоит предлагать как один из вариантов, а не как единственный. Форма с паролем и подтверждением почты остаётся нужной, пусть и на втором плане. Такой подход покрывает оба типа аудитории и не ставит доступ к сервису в зависимость от чужой инфраструктуры.
Протокол делегирования доступа получился редким примером технологии, которая решила конкретную боль и не пыталась решить всё остальное. Он не хранит пароли, не проверяет личности и не заменяет систему управления пользователями. Зато он аккуратно проводит границу между тем, кому доверено, и тем, что доверено, а эта граница и есть основа спокойного входа в любой сервис.