Архитекторы ничего не выдумывают. Они трансформируют реальность.

Алваро Сиза Виэйра

Много всего уже сказано и написано про фреймворк авторизации OAuth 2.0 с 2012 года. И, казалось бы, все давно его знают, используют, все должно работать надежно и безопасно.
Но, как обычно, на практике все иначе. В работе в реальности приходится сталкиваться с небезопасными реализациями процессов авторизации и аутентификации. Огорчает, что по статистике Россия занимает непочетное первое место по своей уязвимости.
Почему же так получается? Предлагаю вместе со мной и авторами драфта OAuth 2.1 от июля 2020 года сделать небольшую работу над ошибками. Это и будет отражением, на мой взгляд, того, по какому пути развития идет фреймворк OAuth 2.

Также спешу предупредить строгого читателя, что в данной статье я затрону только вопросы и сложности, связанные с реализациями по OAuth 2.0. Я не ставлю цели обозначить все проблемы с безопасностью в России в ИТ, и почему этот вопрос требует особого пристального внимания сегодня.

Введение


Стоит ли винить во всем разработчиков, как это принято делать чаще всего? На мой взгляд, не стоит. У разработчика часто стоит задачей реализовать ту или иную функциональность по неким требованиям. Посмотрим же внимательнее на OAuth 2.0.

Фреймворк предлагает воспользоваться правилами по организации потоков авторизации:


А также требования к форматам обмена.

«Что в описании стандарта может приводить к небезопасным реализациям на практике?»
Я пока оставлю этот вопрос открытым, и предлагаю читателю самостоятельно при дальнейшем изучении OAuth 2.0 и прочтении статьи делать свои выводы. На протяжении этой статьи я же буду приводить свое видение ответа на этот вопрос.

Понятия и термины


Перед рассмотрением отдельно каждого из потоков, дадим определения базовой терминологии, используемой в стандарте (читателю, знакомому с терминологией OAuth 2.0, данный абзац можно пропустить).

Таблица 1. Базовые термины 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 Приложение на основе пользовательского агента — это публичный клиент, в котором клиентский код загружается с веб-сервера и выполняется в пользовательском агенте (например, веб-браузере) на устройстве, используемом владельцем ресурса. Учетные данные легкодоступны (и часто видны) владельцу ресурса.
  • SPA App, 
  • Javascript application with a backend (частные примеры приложений: Angular front-end с .NET backend, или  React front-end с Spring Boot backend.) 
  • JavaScript Applications without a Backend
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.
Давайте добавим ресурсный сервер для устранения этой неточности в описание потока (на рисунках я буду приводить только положительные сценарии, чтобы не перегружать диаграммы условиями):

Authorization Code Grant with a Resource Server (Resource Server != Authorization Server)


Также стоит отдельно заметить, что если серверная часть клиент-серверного приложения выполняет запросы к 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).
Client Credentials:


Но здесь то уж не могли ничего напутать, правда?
Давайте читать дальше.
В части описания ответа сервера клиенту мы видим такую фразу:
«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.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, валидность токена, идентификацию клиента и т.д.) не ресурсным сервером:

Аuthorization code grant с PKCE и API Gateway


Памятка для разработчиков


Имеющийся сейчас у меня опыт работы с системами, реализующими 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
Рекомендуется:
  1. В обязательном порядке ограничивать список возможных redirect_url.
  2. Для хранения токенов использовать js переменные.
  3. Используется Public Client.
  4. В потоке Authorization Code Grant Flow with PKCE для проверки токенов использовать API Gateway:

Аuthorization code grant с PKCE и API Gateway


Рекомендуется:
  1. ClientID и Secret в обязательном порядке хранить на серверной стороне приложения.
  2. Используется Confidential Client.
  3. Использовать поток Authorization Code Grant Flow (как с PKCE, так допустимо и без):
    Authorization Code Grant with a Resource Server (Resource Server != Authorization Server)


Не стоит:
  1. Реализовывать аутентификацию внутри бизнес-сервисов.
  2. Использовать offline_access.
  3. Передавать токены в path- и query- параметрах.
  4. Реализовывать авторизационный и ресурсный сервер в едином решении.

Не стоит:
  1. Передавать ID Token, Access Token, Refresh Token из серверной части агенту пользователя (браузер), т.е. токен хранится только на стороне бэка.


Приведенные выше заметки, на мой взгляд, могут быть полезны при составлении чек-листа требований к системам, отвечающим за безопасность процессов авторизации/аутентификации пользователей. Возможно, я что-то важное, на ваш взгляд, упускаю. Поможете мне дополнить список требований и ограничений?

Заключение


В заключении, думаю, стоит заметить, что исходя из той динамики, о которой говорилось выше по дополнению OAuth 2.0 RFC и драфтами в течение 9 лет, мы можем ожидать, что со временем, все-таки наши системы станут безопаснее и надежнее. Мы видим, что стандарт становится строже, но в то же время, он и неизбежно трансформируется под влиянием тенденций в разработке. Хотя бдительность я бы предложила не терять и не расслабляться: с новыми требованиями мы можем столкнуться и с новыми нелепыми реализациями из-за двусмысленности добавленных формулировок.

Системный архитектор,
© Ирина Блажина