OAuth 2.0 и OpenID Connect: безопасность
Разбираемся, как «Войти через…» отдаёт доступ без передачи пароля — и где в этой схеме чаще всего возникают дыры.
OAuth 2.0 — протокол делегированной авторизации: он позволяет приложению получить ограниченный доступ к ресурсам пользователя на другом сервисе без передачи пароля. OpenID Connect (OIDC) — слой аутентификации поверх OAuth 2.0, который добавляет проверяемое удостоверение личности (ID-токен).
Важно сразу развести понятия: OAuth 2.0 отвечает на вопрос «что приложению разрешено делать» (доступ к API), а OIDC — на вопрос «кто этот пользователь» (логин). Их часто путают, и именно из путаницы рождаются ошибки: «голый» OAuth берут как способ логина, не проверяя, кому на самом деле выдан токен. Мы разбираем потоки и их защиту концептуально; цель — настроить интеграцию правильно, а не обойти чужую. Тестировать чужие OAuth-серверы без разрешения нельзя.
Зачем это знать защитнику
OAuth/OIDC — это вход во множество приложений через одного провайдера, поэтому ошибка в потоке масштабируется: одна дыра в redirect_uri может отдать токены многих пользователей. Разработчик, понимающий, зачем нужны state и PKCE и почему перенаправление надо проверять строго, не оставит типовых уязвимостей. Эти же признаки — чек-лист для ревью чужой интеграции.
Правильный поток: authorization code + PKCE
Безопасный современный поток для веб- и мобильных клиентов — authorization code с PKCE. Упрощённо по шагам:
1. Клиент генерирует случайный code_verifier и его хеш code_challenge.
2. Редирект пользователя к провайдеру: response_type=code,
client_id, redirect_uri, scope, state (анти-CSRF), code_challenge.
3. Пользователь аутентифицируется у провайдера и подтверждает доступ.
4. Провайдер редиректит обратно на redirect_uri с одноразовым code и тем же state.
5. Клиент НА СЕРВЕРЕ меняет code на токены, прислав code_verifier.
Провайдер проверяет, что хеш verifier совпал с ранее присланным challenge.
6. Клиент получает access_token (и id_token для OIDC), проверяет их и работает.
PKCE (Proof Key for Code Exchange) — расширение, которое привязывает выданный authorization code к тому, кто его запросил: обменять code на токены сможет только владелец исходного
code_verifier.
Зачем это нужно: authorization code короткоживущий, но если он утечёт (например, через логи, реферер или вредоносное приложение на устройстве), без PKCE его можно обменять на токены. PKCE закрывает эту дыру — перехваченный code бесполезен без секрета, который знает только легитимный клиент. Сегодня PKCE рекомендуется для всех типов клиентов, не только мобильных.
Чего избегать. Поток implicit (токен возвращался прямо в URL) и передача пароля пользователя приложению (resource owner password) считаются устаревшими и небезопасными: первый «светит» токеном в адресной строке и истории, второй сводит на нет весь смысл делегирования. Используйте authorization code + PKCE.
Валидация redirect_uri и параметра state
redirect_uri — строгое сравнение по allowlist
redirect_uri — это адрес, куда провайдер вернёт code. Если провайдер (или ваш сервер) принимает произвольный или «похожий» адрес, атакующий уведёт code на свой сайт. Поэтому redirect_uri проверяют по точному совпадению с заранее зарегистрированным списком, без «подстрок» и открытых wildcard:
# УЯЗВИМО: проверка по подстроке — обходится поддоменом/суффиксом
if allowed_host in redirect_uri: # напр. "example.com" найдётся и в "example.com.evil.tld"
do_redirect(redirect_uri)
# БЕЗОПАСНО: точное совпадение с allowlist зарегистрированных URI
REGISTERED = {"https://app.example.com/callback"}
if redirect_uri in REGISTERED:
do_redirect(redirect_uri)
else:
abort(400)
Связанная угроза — open redirect где-либо в приложении: если есть страница, которая редиректит на произвольный URL из параметра, её можно вплести в цепочку и увести code. Любые редиректы держите по allowlist.
state — защита от CSRF на колбэке
state — случайное значение, которое клиент кладёт в запрос и проверяет при возврате. Оно привязывает ответ провайдера к конкретной сессии пользователя и не даёт атакующему «подсунуть» свой authorization code в чужой сеанс (CSRF на этапе колбэка). Сгенерировали state, сохранили в сессии, при возврате сверили — не совпало, отклонили. В OIDC дополнительно используют nonce, чтобы связать ID-токен с конкретным запросом.
Как это работает под капотом
Доверие в OAuth держится на том, что обмен code на токены происходит на сервере по защищённому каналу, а не в браузере. Браузер видит лишь короткоживущий authorization code; реальные токены приложение получает «бэкенд-к-бэкенду». PKCE добавляет проверку «тот ли это клиент»: провайдер сравнивает хеш присланного code_verifier с ранее полученным code_challenge. Параметр state закрывает CSRF, строгая проверка redirect_uri не даёт увести code, а валидация токенов (см. урок про JWT для id_token) гарантирует, что приложение приняло удостоверение именно своего провайдера, а не подделку.
Как защититься
- Только authorization code + PKCE. Откажитесь от implicit и password-flow.
- Строгая валидация redirect_uri по точному allowlist; устраните open redirect в приложении.
- Обязательный state (анти-CSRF) и nonce в OIDC; проверяйте их при возврате.
- Минимальные scope. Запрашивайте только нужные разрешения (принцип наименьших привилегий), это ограничивает ущерб при утечке токена.
- Защита токенов. Access-токены короткоживущие; refresh-токены храните серверно и применяйте ротацию. Не кладите токены в URL и в доступное скриптам хранилище без необходимости — предпочтительно серверная сессия или httpOnly-cookie.
- Не путайте авторизацию и аутентификацию. Для логина используйте OIDC и проверяйте
id_token(подпись, издательiss, аудиторияaud, срок). «Голый» access-токен не доказывает, кому он выдан. - Обнаружение. Логируйте отклонённые redirect_uri и несовпадения state — это сигналы попыток атак на поток.
Юридическое напоминание: исследовать OAuth-интеграцию можно на своих приложениях и тестовых клиентах провайдера; атаковать чужие — нет.
Итоги
- OAuth 2.0 — про делегированный доступ («что можно»), OIDC — про аутентификацию («кто это»); их нельзя путать.
- Безопасный поток — authorization code + PKCE; implicit и password-flow устарели.
- redirect_uri проверяйте точным совпадением по allowlist; state (и nonce в OIDC) защищают от CSRF на колбэке.
- Обмен кода на токены — на сервере; запрашивайте минимальные scope, берегите refresh-токены.
- Для логина проверяйте
id_token(подпись, iss, aud, exp), а не «голый» access-токен.