Архитекторы ничего не выдумывают. Они трансформируют реальность.
Алваро Сиза Виэйра
Много всего уже сказано и написано про фреймворк авторизации OAuth 2.0 с 2012 года. И, казалось бы, все давно его знают, используют, все должно работать надежно и безопасно.
Но, как обычно, на практике все иначе. В работе в реальности приходится сталкиваться с небезопасными реализациями процессов авторизации и аутентификации. Огорчает, что по статистике Россия занимает непочетное первое место по своей уязвимости.
Почему же так получается? Предлагаю вместе со мной и авторами драфта OAuth 2.1 от июля 2020 года сделать небольшую работу над ошибками. Это и будет отражением, на мой взгляд, того, по какому пути развития идет фреймворк OAuth 2.
Также спешу предупредить строгого читателя, что в данной статье я затрону только вопросы и сложности, связанные с реализациями по OAuth 2.0. Я не ставлю цели обозначить все проблемы с безопасностью в России в ИТ, и почему этот вопрос требует особого пристального внимания сегодня.
Введение
Стоит ли винить во всем разработчиков, как это принято делать чаще всего? На мой взгляд, не стоит. У разработчика часто стоит задачей реализовать ту или иную функциональность по неким требованиям. Посмотрим же внимательнее на OAuth 2.0.
Фреймворк предлагает воспользоваться правилами по организации потоков авторизации:
- Authorization Code Grant;
- Implicit Grant;
- Resource Owner Password Credentials Grant;
- Client Credentials Grant.
А также требования к форматам обмена.
«Что в описании стандарта может приводить к небезопасным реализациям на практике?»
Я пока оставлю этот вопрос открытым, и предлагаю читателю самостоятельно при дальнейшем изучении OAuth 2.0 и прочтении статьи делать свои выводы. На протяжении этой статьи я же буду приводить свое видение ответа на этот вопрос.
Понятия и термины
Перед рассмотрением отдельно каждого из потоков, дадим определения базовой терминологии, используемой в стандарте (читателю, знакомому с терминологией OAuth 2.0, данный абзац можно пропустить).
Термин OAuth 2.0 Authorization Framework | Описание из OAuth 2.0 | Пример |
---|---|---|
Resource owner (Владелец ресурса) |
Абстрактная сущность, предоставляющая доступ к защищенном ресурсу. Если в качестве этой роли выступает человек, то его называют конечным пользователем (end-user). |
Например, человек, предоставляющий доступ к своим персональным данным, хранящимся на сервере. |
Resource server (Сервер ресурсов) |
Сервер, на котором размещаются защищенные ресурсы, принимающий и отвечающий на защищенные запросы к ресурсам с использованием токена доступа (access token). | Например, сервер KeyCloak, предоставляющий доступ к данным пользователя через REST-сервис (UserInfo Endpoint) |
Client (Клиентское приложение) |
Приложение, выполняющее запросы ресурсов от имени владельца ресурса и с его разрешения. Термин «клиент» не подразумевает каких-либо конкретных характеристик реализации в стандарте (например, выполняется ли приложение на сервере, рабочем столе или других устройствах). | Web Application, Desktop Native Application, Mobile Native Application, SPA App, Javascript application |
Authorization server (Сервер авторизации) |
Сервер, выдающий клиенту токены доступа после успешной аутентификации владельца ресурса и авторизации. | Например, сервер KeyCloak (если говорить про открытое решение KeyCloak, то он может выступать как сервером ресурсов, так и сервером авторизации одновременно. Примечание из OAuth: Сервер авторизации может быть тем же сервером, что и сервер ресурсов, или отдельным решением. Один сервер авторизации может выдавать токены доступа, принимаемые несколькими серверами ресурсов. |
Web application (веб-приложение) |
Веб-приложение — это конфиденциальный клиент, запускаемый на веб-сервере. Владельцы ресурсов получают доступ к клиенту через пользовательский интерфейс HTML, отображаемый в Агенте пользователя на устройстве, используемом владельцем ресурса. Учетные данные клиента, а также любой токен доступа, выданный клиенту, хранится на веб-сервере и недоступен владельцу ресурса. | Традиционное веб-приложение в клиент-серверной архитектуре. |
User-agent-based application | Приложение на основе пользовательского агента — это публичный клиент, в котором клиентский код загружается с веб-сервера и выполняется в пользовательском агенте (например, веб-браузере) на устройстве, используемом владельцем ресурса. Учетные данные легкодоступны (и часто видны) владельцу ресурса. |
|
Native application | Локально устанавливаемое приложение — это публичный клиент, установленный и выполняемый на устройстве, используемом владельцем ресурса. Данные протокола и учетные данные доступны владельцу ресурса. Предполагается, что любые учетные данные аутентификации клиента, включенные в приложение, могут быть извлечены. | Desktop Native Application, Mobile Native Application На различных устройствах, включая: настольный компьютер, телефон, планшет и т. д. |
Application without an authentification flow | Приложение, не участвующее в процессе авторизации/аутентификации пользователей. Выполняет только защищенные запросы к серверам ресурсов. | Бэк-часть клиентского приложения, внешняя система-клиент, выполняющая запрос к сервисам системы. |
Теперь можем перейти к рассмотрению каждого из потоков детальнее.
Критический взгляд на OAuth 2.0
Изучая последовательно стандарт, можно столкнуться с рядом неоднозначных рекомендаций, что, в прочем, не является редким явлением зачастую в документации. Предлагаю сейчас и заняться выявлением этих несоответствий. Хотя кому-то я могу показаться излишне придирчивой. Я этого не исключаю и буду рада услышать встречное мнение читателя.
Authorization Code Grant
Рассмотрим первый из наиболее распространенных в реализации потоков с момента создания стандарта: Authorization Code Grant. В самом начале стандарта его разработчики предлагают ознакомиться с общим, объединяющим все потоки документа представлением. А далее уже рассматривается каждый индивидуально и, в частности, Authorization Code Grant (from the OAuth 2.0 Authorization Framework). Но что мы видим:
На представлении потока Authorization Code Grant почему-то отсутствует ресурсный сервер. Наверное, у многих потребителей стандарта может возникнуть вопрос, что же делать дальше с полученным токеном?
На практике периодически встречалась с реализацией, когда сам поток реализовывали корректно, а далее все запросы к сервисам просто шли непосредственно из браузера (User-Agent) без какого-либо использования токена для защиты. Либо с использованием токена, который от клиента (Client) пробрасывался в браузер сначала, а из браузера (User-Agent) шли запросы к сервисам. Токен, естественно, можно просто было просмотреть в самом браузере.
В стандарте в самом начале есть описание, как использовать токен в общем описании потока. Но как показала практика, не хватает этой детализации и в описании Authorization Code Grant.
Давайте добавим ресурсный сервер для устранения этой неточности в описание потока (на рисунках я буду приводить только положительные сценарии, чтобы не перегружать диаграммы условиями):
Также стоит отдельно заметить, что если серверная часть клиент-серверного приложения выполняет запросы к Resource Server внутри одной защищенной сети, то использование Access токена можно опустить. Запросы будут защищены в рамках одной сети, а Authorization Code Grant будет использоваться непосредственно только для аутентификации пользователя.
Implicit Grant
Представление Implicit Grant из OAuth 2.0 я не стану приводить здесь, с ним вы можете самостоятельно ознакомиться по ссылке.
Изучая последовательно поток Implicit Grant, исполнитель задачи, на мой взгляд, может обратить внимание на фразу в описании стандарта:
«These clients are typically implemented in a browser using a scripting language such as JavaScript.»
— «Ага! Клиент-серверные приложения уходят в прошлое, значит Authorization Code Grant нам не подходит! Давайте Implicit Grant использовать!» — наверное, подумал аналитик или разработчик.
Читаем дальше.
«Because the access token is encoded into the redirection URI, it may be exposed to the resource owner and other applications residing on the same device.»
— «Еще проще! Никаких сложных и запутанных редиректов!» — тоже, наверное, может подумать разработчик или аналитик? Ведь стандарт сам разрешает так делать.
Надо читать дальше? Вроде, и так все понятно…
А дальше мы увидим вот такую рекомендацию:
«See Sections 10.3 and 10.16 for important security considerations when using the implicit grant.»
Если мы пройдем по ссылке 10.16, то первое, что мы прочтем, будет это:
" For public clients using implicit flows, this specification does not provide any method for the client to determine what client an access token was issued to."
А затем описание с рекомендациями, как мы могли бы сами о себе позаботиться. Т.е. получаем, что как бы поток в стандарте описан, но его описание ничего общего с «безопасностью» не имеет. Т.е. сами по себе описываемые последовательности запросов в фреймворке авторизации не гарантируют безопасного взаимодействия. Могут потребоваться дополнительные меры по ее организации.
И OAuth 2.0 — это не всегда про безопасность, несмотря на устоявшееся мнение. Но все ли читают рекомендации в сносках?
Resource Owner Password Credentials Grant
Визуальное представление Resource Owner Password Credentials Grant кажется очень простым.
Но в самом параграфе нам сразу написали:
«The authorization server should take special care when enabling this grant type and only allow it when other flows are not viable.»
Думаю, эта фраза многих уберегла от реализации данного потока на практике. Не будем долго задерживаться на Resource Owner Password Credentials Grant.
Client Credentials Grant
Данный поток подразумевает взаимодействие клиента (Client) с авторизационным сервером (Authorization Server) посредством обмена клиентского идентификатора (Client ID -логина) и секрета (Secret — пароля для системы) на токен доступа (access token).
Но здесь то уж не могли ничего напутать, правда?
Давайте читать дальше.
В части описания ответа сервера клиенту мы видим такую фразу:
«A refresh token SHOULD NOT be included.»
Но что значит «не следует»? Т.е. вроде как, и можно использовать, т.е. какие-то сценарии могут потребовать его наличия? А на практике мы имеем, что системы, которые реализуются по OAuth 2.0, вынуждены реализовывать такую возможность, так как стандартом она не исключается.
И здесь надо вспомнить, для чего, в принципе, были придуманы «Refresh token» и «Access token».
У Access token есть ограниченный срок действия, и когда он истекает, клиент (Client) в обмен на «Refresh token» запрашивает новый «Access token». «Refresh token» всегда может использоваться один и тот же, либо с каждым новым «Access token» передаваться и новый «Refresh token» должен, что безопаснее, но сложнее в реализации.
Если вернуться к описанию потока Authorization Code Grant, то там мы можем увидеть реальную необходимость его использования: в случае, когда по каким-либо причинам злоумышленнику удалось украсть Access token в обмен на код авторизации (это возможно, так как код авторизации передается приложению через обмен с браузером (или другим пользовательским агентом), а Client ID и Secret по каким-то причинам не используются при запросе токена доступа (или их смогли украсть ранее), то из-за ограниченности периода действия Access token, у злоумышленника будет столько времени навредить, сколько действует Access token, и пока в обмен на Refresh token не будет запрошен новый Access token доверенным клиентом.
И да, Authorization Code Grant тоже не гарантирует полной безопасности, хотя при его корректной реализации, злоумышленнику не так то просто вклиниться в поток и что-то украсть или сломать, особенно еще и с учетом проверок пользовательских и клиентских сессий, которые хранятся обычно на сервере авторизации, и о которых я ничего не рассказываю здесь.
А если мы посмотрим на Client Credentials Flow, то здесь, если злоумышленник украдет Client ID и Secret, то Refresh token нам не навредит, но уже никак и не поможет. Поэтому мы имеем избыточность и излишнюю сложность в реализациях.
OAuth 2.0 -> OAuth 2.1. Что дальше?
Итак, мы с некой долей скептицизма рассмотрели авторизационные потоки, которые нам предлагает фреймворк OAuth 2.0. Давайте же теперь разбираться и отвечать на вопрос, поставленный в заголовке к самой статье. В каком направлении и как развивается OAuth 2.0?
За период, начиная с момента перевода OAuth 2.0 из черновика в статус стандарта, до настоящего времени разработчики стандарта продолжали активно делать работу над ошибками, которая появлялась в свет в виде новых RFC и черновиков в дополнение к OAuth 2.0:
- OAuth 2.0 for Native Apps (RFC 8252),
- Proof Key for Code Exchange (RFC 7636),
- OAuth for Browser-Based Apps,
- OAuth 2.0 Security Best Current Practice.
Итогом почти десятилетней работы стало появление драфта OAuth 2.1, период действия которого истекает 31.01.2021.
И здесь интригующий напрашивается вопрос: «будет ли продлен драфт или перейдет в статус стандарта?»
Драфт консолидировал в себе изменения всех более поздних публикаций. Наиболее значимые из них:
(1) Потоки Implicit grant (response_type=token) и Resource Owner Password Credentials исключаются из документа.
(2) Аuthorization code grant расширили кодом PKCE (RFC7636) таким образом, что единственный вариант использования Аuthorization code grant в соответствии с этой спецификацией требует добавления механизма PKCE;
Clients MUST use «code_challenge» and «code_verifier» and authorization servers MUST enforce their use except under the conditions described in Section 9.8. In this case, using and enforcing «code_challenge» and «code_verifier» as described in the following is still RECOMMENDED.
Clients MUST prevent injection (replay) of authorization codes into the authorization response by attackers. To this end, using «code_challenge» and «code_verifier» is REQUIRED for clients and authorization servers MUST enforce their use, unless both of the following criteria are met:
* The client is a confidential or credentialed client.
* In the specific deployment and the specific request, there is reasonable assurance for authorization server that the client implements the OpenID Connect «nonce» mechanism properly.
(3) Redirect URIs должны будут всегда явно задаваться на сервере.
(4) Refresh токены для публичных клиентов должны ограничиваться, либо использоваться не более 1 раза.
С учетом (1) и (2), наибольшего внимания, на мой взгляд, заслуживает Proof Key for Code Exchange сейчас.
Как мы и говорили выше, все чаше мы уже отходим от реализаций классических проверенных временем клиент-серверных архитектур (хотя я сама призываю всегда не следовать за модой, а выбирать решение в зависимости от поставленной задачи). И разработчики RFC предлагают измененные решения для трансформирующейся реальности.
К сожалению, в OAuth 2.1 представления потоков не изменились, дополнились только описания к ним. Поэтому я не буду уже приводить вид из стандарта (с теми же граблями в виде отсутствующего ресурсного сервера), а сразу сделаю боле детальное описание.
На диаграмме последовательностей вызовов ниже приведен случай для приложения без классического бэкенда (намеренно указывается JSClient). А также видим, что в случае, когда у нас появляется и ресурсный сервер на диаграмме, то мы вынуждены добавлять API шлюз для выполнения более сложных проверок (URI, валидность токена, идентификацию клиента и т.д.) не ресурсным сервером:
Памятка для разработчиков
Имеющийся сейчас у меня опыт работы с системами, реализующими OAuth 2, хотела бы оставить в статье, так как надеюсь, что он может оказаться кому-то полезен и позволит качественнее защищать наши с вами данные в сети.
Позволю себе обозначенные правила к авторизационным потокам и приложениям, которые их реализуют, разложить в виде матрицы принятия решений:
Приложение по OAuth 2.0 | OAuth2/OIDC Authorization code Grant | OAuth2/OIDC Authorization code Grant with PKCE | Client Credentials Grant |
---|---|---|---|
Web application with a Confidential Client | + | + | + |
User-agent-based application: JavaScript Applications without a Backend with a Public Client |
- | + | - |
User-agent-based application: Javascript application with a Backend with a Confidential Client |
+ | + | + |
Application without an authentification flow with a Confidential Client | - | - | + |
Для того или иного приложения и выбираемого подхода существуют свои плюсы и минусы. Вкратце посмотрим на «JavaScript application with no backend» и «JavaScript application with a backend» и дадим ключевые рекомендации к реализациям:
JavaScript application with no backend | JavaScript application with a backend |
---|---|
Рекомендуется:
Аuthorization code grant с PKCE и API Gateway
|
Рекомендуется:
|
Не стоит:
|
Не стоит:
|
Приведенные выше заметки, на мой взгляд, могут быть полезны при составлении чек-листа требований к системам, отвечающим за безопасность процессов авторизации/аутентификации пользователей. Возможно, я что-то важное, на ваш взгляд, упускаю. Поможете мне дополнить список требований и ограничений?
Заключение
В заключении, думаю, стоит заметить, что исходя из той динамики, о которой говорилось выше по дополнению OAuth 2.0 RFC и драфтами в течение 9 лет, мы можем ожидать, что со временем, все-таки наши системы станут безопаснее и надежнее. Мы видим, что стандарт становится строже, но в то же время, он и неизбежно трансформируется под влиянием тенденций в разработке. Хотя бдительность я бы предложила не терять и не расслабляться: с новыми требованиями мы можем столкнуться и с новыми нелепыми реализациями из-за двусмысленности добавленных формулировок.
Системный архитектор,
© Ирина Блажина
Wyrd
Можете пожалуйста обьяснить почему? В браузере ведь должен быть какой-то секрет чтобы делать повторные обращения к сервису без повторного логина. Чем авторизационная кука / айди сессии лучше токенов? они ведь дают одинаковый уровень доступа
Wyrd
Если что я вот про такой подход: https://hasura.io/blog/best-practices-of-using-jwt-with-graphql/
ultrinfaern
Насколько я понимаю, так как есть бэк, то вся обработка там, а фронт это только картинки, ну и соответсвенно между беком и фронтом можно просто воспользоваться обычными сессиями. Поэтому передача на фронт креденшионалов лишена смысла.
Wyrd
Если на беке микросервисы, то не лишена — JWT позволяет обойтись без центрального хранилища сессий и проверять access токены просто валидируя подпись (конечно, если вам не нужен механизм отзыва access токенов, или его можно игнорировать для большинства вызовов проверяя отзыв только перед действительно страшными операциями из серии «удалить все, я уверен»).
Кроме того, это позволяет перенести проверку токенов в API Gateway как указано в статье, и вообще вынести аутентификацию в отдельный Identity Provider сервис (есть стандартные реализации IdP). Выделение IdP, в свою очередь, позволяет легко и непринуждённо добавлять всякие 3-party SSO, двухфакторные аутентификации и т.п. (для других сервисов ничего не меняется) + IdP отвечает за refresh токены, их обработку и отзыв.
Ну и… ведь в “JS Only App” токены как я понимаю как раз в браузере и храниться (хотя явно этого я в статье не увидел, но вроде подразумевается) — непонятно почему в одном месте это плюс, а в другом минус.
Напоследок, мне показалось есть путаница с PKCE — насколько я понимаю оно к хранению/токенам никакого отношения не имеет, это просто более безопасный способ получить токен, который закрывает некоторые потенциальные атаки на стадии логина построенные вокруг перехвата authorization code. Сами токены при этом все те же.
ultrinfaern
Там скорее всего имеется ввиду что бэк только для фронта и логики там практически нет. А вот дальше где-то еще и микросервисы и все остальное.
Archi-Blair Автор
Доброе утро!
Спасибо за ваш вопрос. Здесь важно обратить внимание на то, что эта рекомендация дается для приложения "JavaScript application with a backend".
Сейчас токены чаще всего как случайный набор символов не используются. Многие решения формируют токены в формате JWT. И в токенах может содержаться значимая информация (встречаются реализации с ФИО, телефонами, email и т.д.).
Поэтому для JavaScript application with a backend стоит использовать:
Для классического Authorization Code Grant схему дополнила информацией по тому, в какой момент еще формируются пользовательская и клиентская сессия:
(надеюсь, читаемо)
andreyverbin
Лучше refresh token положить в куку, а куку зашифровать. Тогда JS не сможет залезть куда не нужно, а сервер сможет обновить сессию на ресурсном сервере когда потребуется.