Вопрос, который решает всё

делит клиентов на два вида, и разделительная черта — единственный вопрос: может ли этот клиент хранить секрет? RFC 6749 называет два ответа конфиденциальными и публичными клиентами, и то, какой вы, определяет, как вы аутентифицируетесь, какие потоки безопасны и нужен ли вам . Почти всякая ошибка проектирования OAuth на уровне приложения восходит к обращению с публичным клиентом так, будто он мог бы хранить секрет, который на деле не может защитить.

Конфиденциальные клиенты могут хранить секрет

Конфиденциальный клиент выполняется где-то, где пользователь не может добраться до его кода или хранилища, классически — серверное веб-приложение. Ему выдаётся секрет клиента при регистрации, и он использует этот секрет, чтобы аутентифицироваться перед эндпоинтом токена сервера авторизации. Поскольку секрет живёт только на сервере, сервер авторизации может быть уверен, что запрос токена действительно исходит от зарегистрированного клиента. Это сильный случай: клиент доказывает свою идентичность чем-то, что знает только он.

Публичные клиенты не могут

Публичный клиент выполняется там, где его код и данные видны пользователю или атакующему: одностраничное приложение в браузере, нативное мобильное или настольное приложение, что угодно, доставленное на устройство. Определяющая проблема в том, что любой секрет, который вы встраиваете в такой клиент, не секрет, потому что любой может извлечь его из доставленного бандла или бинарника. Так что публичный клиент вообще не может аутентифицироваться секретом клиента; нет ничего, что он мог бы хранить и чего атакующий не мог бы тоже получить. Это меняет то, что сервер авторизации может предполагать, и это причина, по которой публичным клиентам нужна дополнительная защита в потоке кода авторизации.

Почему публичные клиенты были уязвимы

Поток кода авторизации вручает клиенту одноразовый код в редиректе, который клиент затем обменивает на токены. Для конфиденциального клиента этот обмен защищён секретом клиента. Для публичного клиента без секрета атакующий, способный перехватить редирект, например вредоносное приложение, зарегистрированное на ту же пользовательскую схему URL на мобильном устройстве, может украсть код и сам его погасить. Это атака перехвата, которую статья PKCE описывает подробно, и это ровно та брешь, которую оставляет открытой отсутствующий секрет клиента.

PKCE заполняет брешь — для всех

PKCE закрывает эту дыру без заранее общего секрета. Клиент изобретает свежий случайный verifier кода на каждый запрос, отправляет лишь его хеш (challenge кода), запрашивая код, и раскрывает verifier лишь при его погашении. Атакующий, перехвативший код, никогда не видел verifier, так что украденный код бесполезен. Это ввели для публичных клиентов, но рекомендации с тех пор расширились: OAuth 2.1 рекомендует PKCE для потока кода авторизации независимо от типа клиента, потому что он добавляет эшелонированную защиту даже там, где секрет клиента присутствует. Практическое правило — использовать код авторизации плюс PKCE везде и перестать рассуждать о том, строго ли он вам нужен.

Неявный поток на пенсии

Публичные браузерные приложения когда-то использовали неявный поток, который возвращал токены прямо в редиректе, чтобы избежать обмена кода. Этот дизайн выставлял токены в URL и истории браузера и теперь устарел; OAuth 2.1 его удаляет. Современный ответ для одностраничного приложения тот же, что и для всех остальных: поток кода авторизации с PKCE. Если вам встретится совет использовать неявный поток, считайте его устаревшим.

У нативных приложений свои ловушки

Нативные и мобильные приложения — публичные клиенты с особой опасностью: редирект обратно в приложение. Пользовательские схемы URL могут быть заявлены другими приложениями на устройстве, что и делает перехват возможным, поэтому рекомендация лучшей практики для нативных приложений (RFC 8252) — использовать заявленные HTTPS-редиректы там, где платформа их поддерживает, выполнять поток в системном браузере, а не во встроенном веб-представлении, и всегда применять PKCE. Для устройств с ограниченным вводом вроде телевизора, где нет вообще браузера, в который перенаправлять, поток авторизации устройства (RFC 8628) позволяет пользователю авторизоваться на отдельном телефоне или ноутбуке.

В сомнении считайте публичным

Сквозная мысль в том, что тип клиента — это не ярлык, который вы выбираете для удобства; это факт о том, где выполняется ваш код и что он может защитить. Если клиент доставляется пользователю, он публичный, не может хранить секрет и нуждается в PKCE. Если он выполняется только на вашем сервере, он может быть конфиденциальным и аутентифицироваться секретом, и ему всё равно следует использовать PKCE ради дополнительного запаса. Решите, в каком мире живёт ваш клиент, и остальная часть его модели безопасности OAuth следует из этого единственного честного ответа.