Регистрация на новом сайте требует придумать пароль, а потом его запомнить. Через неделю человек возвращается и не помнит, какой из трёх привычных вариантов он выбрал. Кнопка входа через уже знакомый сервис снимает эту возню одним нажатием, и за внешней простотой скрывается целый протокол делегирования доступа, который разрабатывался несколько лет и пережил не одну волну исправлений.
Появление OAuth связано с нехваткой стандарта для передачи прав доступа
История началась в 2006 году, когда разработчик одной платформы трудился над единым входом, а владелец сервиса закладок искал способ разрешить сторонним мини-приложениям доступ к данным своих пользователей. Готового открытого стандарта для управления доступом тогда не существовало.
Группа разработчиков собралась в апреле 2007 года и к июлю написала первый вариант спецификации, а третьего октября 2007 года вышла версия 1.0.
В ноябре 2008 года организация по стандартизации интернета приняла протокол в работу, в апреле 2010 года первая версия вышла отдельным документом, а с августа того же года сторонние приложения одной известной платформы обязали использовать новый механизм.
Версия 2.0 строилась на опыте эксплуатации первой, но оказалась с ней несовместимой: она получила номер RFC 6749 и вышла в октябре 2012 года, а документ о работе с токенами доступа тогда же под номером RFC 6750. Оба определяют стандарты, а не рекомендации.
Обновления продолжаются: документы о применении протокола в мобильных приложениях, о защите от перехвата кода и о лучших практиках безопасности постепенно сводятся в единую версию 2.1, работа над ней ещё не завершена.
Протокол решает задачу делегирования, а не проверки личности
Главная путаница связана с назначением технологии. Расшифровка названия как открытой авторизации неточна: речь идёт о делегировании доступа, когда владелец ресурса разрешает стороннему приложению работать с частью своих данных, не передавая ему пароль.
Роли распределены чётко: владелец ресурса это человек, клиент это приложение, которое запрашивает доступ, сервер авторизации выдаёт разрешения и токены, сервер ресурсов хранит защищённые данные и проверяет предъявленные токены.
Ключевой момент: пароль остаётся на сервере авторизации, клиент его не видит. Вместо него выдаётся токен, то есть ограниченный по времени и объёму пропуск, который можно отозвать, не меняя пароль пользователя.
Именно поэтому протокол удобно использовать для входа. Получив подтверждение, что человек прошёл проверку у поставщика учётных данных, приложение создаёт или находит свою учётную запись и открывает сессию. Слой, описывающий передачу сведений о личности, называется 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, 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()
return establish_session(token.get("userinfo"))
Здесь видно сразу три обязательных шага: генерация состояния, его сверка при возврате и получение сведений о пользователе уже после обмена кода. Попытка доверять данным из адресной строки до обмена считается грубой ошибкой.
Вторая деталь: после установления сессии приложение обычно больше не нуждается в исходном токене, и если доступ к сторонним данным не требуется, токен нужно забыть, а не складывать в базу. Меньше сохранённых секретов означает меньший ущерб при взломе.
Третья деталь касается выхода: локальный выход завершает сессию приложения, но не сессию у поставщика учётных данных, и пользователь удивляется, когда повторное нажатие кнопки снова пускает его внутрь без пароля. Честный интерфейс либо предлагает отдельное действие для выхода у поставщика, либо прямо сообщает о разнице.
Четвёртая деталь это обработка отзыва. Пользователь имеет право лишить приложение доступа у себя в настройках. Приложение обязано корректно пережить ситуацию, когда токен обновления больше не работает: показать понятное сообщение и предложить войти заново, а не падать с невнятной ошибкой.
Польза и риски такого входа видны на длинной дистанции
Плюсы очевидны: скорость регистрации растёт, ведь исчезает форма с паролем, нагрузка на поддержку падает, потому что восстановление забытого пароля больше не требуется, а учётные данные защищены лучше, поскольку пароль не хранится в приложении и не может утечь вместе с его базой.
Есть и обратная сторона. Человек попадает в зависимость от поставщика: если сервис входа недоступен, войти не получится. Приложение получает сведения о пользователе, объём которых определяется запрошенными правами, и злоупотребление этим доверием бьёт по репутации. Наконец, часть пользователей принципиально не желает связывать учётные записи между сервисами.
Отсюда вывод: вход через сторонний сервис стоит предлагать как один из вариантов, а не единственный. Форма с паролем остаётся нужной, пусть и на втором плане, такой подход покрывает оба типа аудитории и не ставит доступ в зависимость от чужой инфраструктуры.
Протокол делегирования доступа получился редким примером технологии, которая решила конкретную боль и не пыталась решить всё остальное. Он не хранит пароли, не проверяет личности и не заменяет систему управления пользователями. Зато он аккуратно проводит границу между тем, кому доверено, и тем, что доверено, а эта граница и есть основа спокойного входа в любой сервис.