Информационная система Dodo IS состоит из 44 различных сервисов, таких как Трекер, Кассы ресторана или Базы знаний и многих других. Чтобы не отвлекаться на несколько аккаунтов, 3 года назад мы написали сервис Auth для реализации сквозной аутентификации, а сейчас пишем уже вторую версию, в основе которого лежит стандарт авторизации OAuth 2.0. Этот стандарт довольно сложный, но если у вас сложная архитектура с множеством сервисов, то OAuth 2.0 вам пригодится при разработке своего сервиса аутентификации. В этой статье я постарался рассказать о стандарте максимально просто и понятно, чтобы вы сэкономили время на его изучение.


 

Задача Auth


Проблема авторизации в десятках сервисов встречалась ещё несколько лет назад — в начале «эпохи распила монолита». Эту проблему решили новым сервисом, который назвали – Auth. Он помог реализовать бесшовную аутентификацию в различных сервисах и перенести данные о пользователях в отдельные базы данных. 

У сервиса Auth есть три основные задачи:

  • Единая точка аутентификации (SSO) для всех сервисов системы. Сервисы не хранят учётные данные, а доверяют это одному выделенному сервису.
  • Безопасный и гранулированный доступ к ресурсам. Безопасный, потому что пароли хранятся в одном месте и максимально защищены. Гранулированный, так как владельцы сервисов могут настраивать доступ к ресурсам как они захотят, опираясь на данные, пришедшие из сервиса аутентификации.
  • Централизованное управление пользователями и доступом. Благодаря тому, что вся информация о пользователе хранится в сервисе аутентификации, мы можем управлять пользователями централизованно.

Проблемы


Первая версия Auth — часть монолита. Он использует свой собственный протокол общения с сервисами. Такая «схема» была необходима в тот момент, но за несколько лет работы проявились проблемы.

Auth — часть монолита. Следовательно, сервис привязан к релизному циклу, что лишает возможности независимой разработки и деплоя. Кроме того, придется разворачивать весь монолит, если захотелось развернуть Auth, например, при масштабировании сервиса.

Dodo IS зависит от Auth. В старой реализации внешние сервисы обращаются к Auth при каждом действии пользователя, чтобы валидировать данные о нём. Настолько сильная привязка может привести к остановке работы всей Dodo IS, если Auth «приляжет» по какой-то причине.

Auth зависит от Redis. Притом достаточно сильно — неисправность работы Redis’а приведёт к падению Auth’а. Мы используем Azure Redis, для которого заявленный SLA 99,9%. Это значит, что сервис может быть недоступен до 44 минут в месяц. Такие простои не позволительны.

Текущая реализация Auth использует свой протокол аутентификации, не опираясь на стандарты. В большинстве своих сервисов мы используем C# (если говорим о backend) и у нас нет проблем с поддержкой библиотеки для нашего протокола. Но если вдруг появятся сервисы на Python, Go или Rust, разработка и поддержка библиотек под эти языки потребует дополнительных затрат времени и принесет дополнительные сложности.

Текущий Auth использует схему Roles Based Access Control, которая базируется на ролях. Обычно с ролью выдаётся полный доступ к определённому сервису, вместо привязки к конкретному функционалу. Например, в пиццериях есть заместители управляющего, которые могут вести определенные проекты: составлять графики или учитывать сырьё. Но у нас нет выдачи прав на конкретные компоненты системы. Приходится выдавать полный доступ к сервису, чтобы сотрудники могли получить доступ к составлению графиков или настройкам какого-либо компонента учёта.

Проблемы подтолкнули к тому, чтобы спроектировать и написать новую версию Auth. На старте проекта мы потратили 3 недели только на изучение стандартов авторизации и аутентификации OAuth 2.0 и OpenID Connect 1.0. 

Примечание. Утрированно, статья — это пересказ RFC, который приходилось перечитывать несколько раз, чтобы понять, что происходит вокруг. Здесь я постарался уйти от этой сложности и рассказать всё максимально просто, структурировано, кратко и без описания сложных вещей, например, какие символы может содержать в себе ответ сервиса. В отличии от RFC, прочитав это один раз, можно во всём разобраться. Надеюсь, статья будет полезна и сэкономит время при выборе решения для реализации сервиса аутентификации, а может, кого-то заставит задуматься о его необходимости.

Что такое ОAuth2.0?


Разработку нового Auth мы решили начать с изучения доступных протоколов и технологий. Самый распространённый стандарт авторизации — фреймворк авторизации OAuth2.0. 

Стандарт был принят в 2012 году, и за 8 лет протокол меняли и дополняли. RFC стало настолько много, что авторы оригинального протокола решили написать OAuth 2.1, который объединит все текущие изменения по OAuth 2.0 в одном документе. Пока он на стадии черновика.

Актуальная версия OAuth описанна в RFC 6749. Именно его мы и разберем. 

OAuth 2.0 — это фреймворк авторизации.

Он описывает, как должно реализовываться взаимодействие между сервисами для обеспечения безопасной авторизации. Многие нюансы описаны достаточно подробно, например, flow взаимодействия узлов между собой, но некоторые отдаются на откуп конкретной реализации.

Особенности:

  • Разделение сущности пользователя и приложения, запрашивающего доступ. Благодаря этому разделению мы можем управлять правами приложения отдельно от прав пользователя. 

  • Вместо привычных логина и пароля, которые имеют определенный набор прав и время жизни, мы получаем доступ к ресурсам с помощью случайно сгенерированных строк — токенов.
  • Можно выдавать права максимально точечно, опираясь на собственные пожелания, а не на заранее определённый набор прав.

Разберёмся подробнее в особенностях.

Роли


В OAuth 2.0 определены четыре роли:

  • Resource owner — сущность, которая имеет права доступа на защищённый ресурс. Сущность может быть конечным пользователем или какой-либо системой. Защищённый ресурс — это HTTP endpoint, которым может быть что угодно: API endpoint, файл на CDN, web-сервис.
  • Resource server — сервер, на котором хранится защищённый ресурс, к которому имеет доступ resource owner.
  • Client. Это приложение, которое запрашивает доступ к защищённому ресурсу от имени resource owner и с его разрешения — с авторизацией. 
  • Authorization server — сервер, который выдаёт клиенту токен для доступа к защищённому ресурсу, после успешной авторизации resource owner.

Каждый участник взаимодействия может совмещать в себе несколько ролей. Например, клиент может быть одновременно resource owner, и запрашивать доступ к своим же ресурсам. Схему взаимодействия рассмотрим дальше.

Важно: клиент должен быть заранее зарегистрирован в сервисе. Как это сделать?

Регистрация клиента


Способ регистрации клиента, например, ручной или service discovery, вы выбираете сами, в зависимости от фантазии конкретной реализации. Но при любом способе при регистрации, кроме ID клиента, должны быть обязательно указаны 2 параметра: redirection URI и client type.

Redirection URI — адрес, на который отправится владелец ресурса после успешной авторизации. Кроме авторизации, адрес используется для подтверждения, что сервис, который обратился за авторизацией, тот, за кого себя выдаёт.

Client type — тип клиента, от которого зависит способ взаимодействия с ним. Тип клиента определяется его возможностью безопасно хранить свои учётные данные для авторизации — токен. Поэтому существует всего 2 типа клиентов:

  • Confidential — клиент, который может безопасно хранить свои учётные данные. Например, к такому типу клиентов относят web-приложения, имеющие backend.
  • Public — не может безопасно хранить свои учётные данные. Этот клиент работает на устройстве владельца ресурса, например, это браузерные или мобильные приложения.

Токены


Токен в OAuth 2.0 — это строка, непрозрачная для клиента. Обычно строка выглядит как случайно сгенерированная — её формат не имеет значения для клиента. Токен — это ключ доступа к чему-либо, например, к защищённому ресурсу (access token) или к новому токену (refresh Token).

У каждого токена своё время жизни. Но у refresh token оно должно быть больше, т.к. он используется для получения access token. Например, если срок жизни access token около часа, то refresh token можно оставить жить на целую неделю. 

Refresh token опционален и доступен только для confedential клиентов. Пользуясь опциональностью токена, в некоторых реализациях время жизни access token сделано очень большим, а refresh token вообще не используется, чтобы не заморачиваться с обновлением. Но это не безопасно. Если access token был скомпрометирован, его можно обнулить, а сервис получит новый Access token с помощью refresh token. В случае, если refresh token нет, то потребуется проходить процесс авторизации заново.

За access token закреплён определённый набор прав доступа, который выдаётся клиенту во время авторизации. Давайте разберёмся, как выглядят права доступа в OAuth 2.0.

Права доступа


Права доступа выдаются клиенту в виде scope. Scope – это параметр, который состоит из разделённых пробелами строк — scope-token.

Каждый из scope-token представляет определённые права, выдающиеся клиенту. Например, scope-token doc_read может предоставлять доступ на чтение к какому-то документу на resource server, а employee — доступ к функционалу приложения только для работников фирмы. Итоговый scope может выглядеть так: email doc_read employee.

В OAuth 2.0 мы сами создаём scope-token, настраивая их под свои нужды. Имена scope-token ограничиваются только фантазией и двумя символами таблицы ASCII — " и \.

На этапе регистрации клиента, в настройках сервиса авторизации клиенту выдаётся стандартный scope по умолчанию. Но клиент может запросить у сервера авторизации scope, отличный от стандартного. В зависимости от политик на сервере авторизации и выбора владельца ресурса, итоговый набор scope может выглядеть совсем иначе. В дальнейшем, после авторизации клиента, владелец ресурсов может отобрать часть прав без повторной авторизации сервиса, но, чтобы выдать дополнительные разрешения, потребуется повторная авторизация клиента.

Абстрактный OAuth 2.0. Flow c применением Access token


Мы рассмотрели роли, рассмотрели виды токенов, а также как выглядят scope. Посмотрим на flow предоставления доступа к сервису.

Ниже представлена абстрактная схема (или flow) взаимодействия между участниками. Все шаги на данной схеме выполняются строго сверху вниз. Разберём детальнее.



  • Client отправляет запрос на доступ к требуемому ресурсу resource owner.
  • Resource owner передаёт обратно клиенту authorization grant, который подтверждает личность resource owner и его права на ресурс, доступ к которому запрашивает client. В зависимости от flow это может быть токен или учётные данные.
  • Client отправляет authorization grant, полученный в предыдущем шаге authorization server, ожидая от него Access token для доступа к защищённому ресурсу. 
  • authorization server убеждается в валидности authorization grant, после чего отсылает access token клиенту в ответ.
  • Получив access token, клиент запрашивает защищённый ресурс у resource server. 
  • Resource server убеждается в корректности access token, после чего предоставляет доступ к защищённому ресурсу.

Клиент получает одобрение от resource owner, на основе которого ему выдаётся доступ к ресурсу. Всё просто. А будет ли так же просто, если мы добавим в эту схему работу с refresh token?

Абстрактный OAuth 2.0. Flow c применением Refresh token


Первый и второй шаги опущены из данной схемы — они ничем не отличаются от схемы абстрактного flow выше.



Схема подробнее:

  • Client приходит c authorization grant к authorization server и просит предоставить ему access token и refresh token.
  • Authorization server убеждается, что с authorization grant всё нормально и возвращает клиенту запрошенные access token и refresh token.
  • Client с access token запрашивает защищённый ресурс, пока не получит первую ошибку доступа к ресурсу — invalid token error.
  • После получения ошибки доступа, клиент идет к authorization server с refresh token и просит заменить просроченный access token на новый. 
  • В ответ клиент получает новый access token, а также новый refresh token, либо продлевается время жизни старого refresh token. 

Что такое grant?


Grant — это данные, которые представляют из себя успешную авторизацию клиента владельцем ресурса, используемые клиентом для получения access token.

Например, когда мы где-либо аутентифицируемся с помощью Google, перед глазами всплывает уведомление. В нём говорится, что такой-то сервис хочет получить доступ к данным о вас или к вашим ресурсам (выводятся запрашиваемые scope-token). Это уведомление называется «Consent Screen».

В момент, когда нажимаем «ОК», в базу данных попадает тот самый grant: записываются данные о том, что такой-то пользователь дал такие-то доступы такому-то сервису. Клиент получает какой-то идентификатор успешной аутентификации, например строку, которая ассоциируется с данными в базе данных.

Существует 4 + 1 способа получения grant — grant type:

  • Authorization code — используется для confedencial клиентов — web-сервисов.
  • Client credentials — используется для confedential клиентов, которые запрашивают доступ к своим ресурсам или ресурсам, заранее согласованным с сервером авторизации.
  • Implicit — использовался public-клиентами, которые умеют работать с redirection URI (например, для браузерных и мобильных приложений), но был вытеснен authorization code grant с PKCE (Proof Key for Code Exchange — дополнительная проверка, позволяющая убедиться, что token получит тот же сервис, что его и запрашивал. Прочитать подробнее — RFC 7636).
  • Resource owner password credentials. В RFC 6819, посвящённому безопасности в OAuth 2.0, данный тип grant считается ненадёжным. Если раньше его  разрешалось использовать только для миграции сервисов на OAuth 2.0, то в данный момент его не разрешено использовать совсем.
  • Device authorization (добавлен в RFC 8628) – используется для авторизации устройств, которые могут не иметь веб-браузеров, но могут работать через интернет. Например, это консольные приложения, умные устройства или Smart TV.

Актуальными можно считать только authorization code (с PKCE), client credentials и device authorization grant, но мы рассмотрим все. Рассматривать grant будем в порядке возрастания сложности понимания.

Client credentials grant flow


Имеет самый простой flow, напоминающий обычную авторизацию на любом сервисе. Она выполняется с помощью учётных данных клиента, которые представляют собой client id и client secret — аналог логина и пароля для пользователя. Так как для аутентификации требуется client secret, который должен соответствующе храниться, данный flow могут использовать только confedential клиенты.



Схема проста: клиент аутентифицируется на сервере авторизации передавая client id и client secret. В ответ получает access token, с которым уже может получить доступ к нужному сервису.

Этот flow требуется, когда клиент пытается получить доступ к своим ресурсам или ресурсам, заранее согласованным с сервером авторизации. Например, сервису А нужно время от времени ходить в сервис Б и актуализировать там данные о количестве пиццерий в сети.

Resource owner password credentials flow


По текущим рекомендациям безопасности описанных в данном RFC, данный flow не рекомендуется использовать вовсе из-за явных проблем с безопасностью.



Resource owner передаёт свой логин и пароль клиенту, например, через формы на клиенте. Клиент, в свою очередь, с помощью него получает access token (и, опционально, refresh token).

Здесь есть проблема. Resource owner просто берёт и отдаёт в открытом виде свой логин и пароль клиенту, что не безопасно. Изначально он был сделан только для клиентов, которым вы доверяете или тех, что являются частью операционной системы. Позже он был разрешён только для миграции с аутентификации по логину и паролю на OAuth 2.0. Текущие рекомендации по безопасности запрещают его использование. 

Authorization code


Самый распространённый flow на данный момент. В основном используется для confidential клиентов, но с появлением дополнительной проверки с помощью PKCE, может применяться и для public-клиентов. 

В данном flow взаимодействие client с resource owner проходит через user-agent (браузер). К user-agent есть одно требование: он должен уметь работать с HTTP-редиректами. Без этого resource owner не сможет попасть к серверу авторизации и вернуться обратно с grant. 



Данный flow сложнее, чем предыдущие, поэтому будем разбирать по шагам. Для начала представим, что мы — resource owner и перешли на страницу сервиса онлайн-обучения, который хочет сохранять результаты обучения к нам в облако. Ему требуется получить доступ к нашему ресурсу, например, определённой директории в облаке. Мы нажимаем на «Авторизоваться» и начинается путешествие по Authorization code grant flow:

  • На первом шаге клиент перенаправляет resource owner с помощью user-agent на страницу аутентификации Authorization server. В URI он указывает client ID и redirection URI. Redirection URI используется для понимания, куда вернуть resource owner после того, как авторизация пройдёт успешно (resource owner выдаст разрешение на scope, запрашиваемый клиентом).
  • Взаимодействуя с сервером авторизации через user-agent, resource owner проходит аутентификацию на сервере авторизации.
  • Resource owner проверяет права, которые запрашивает клиент на consent screen и разрешает их выдачу.
  • Resource owner возвращается клиенту с помощью user-agent обратно на URI, который был указан как redirection URI. В качестве query-параметра будет добавлен authorization code — строка, подтверждающая то, что resource owner выдал необходимые права сервису. 
  • С этим authorization code клиент отправляется на сервер авторизации, чтобы получить в ответ access token (ну и refresh token, если требуется).
  • Сервер авторизации валидирует authorization code, убеждаясь, что токен корректный и выдаёт клиенту access token (и опционально refresh token). С его помощью клиент сможет получить доступ к заветному ресурсу. 

Если представить нас на месте resource owner, то мы видим просто перенаправление на сервер авторизации, аутентифицируемся, подтверждаем доступ на Consent screen и нас отправляет на уже работающий сервис. Например, мы проходим это много раз, когда заходим на сервис под учётной записью Google, Facebook или Apple.

Следующий flow построен на основе этого.

Implicit grant


Это оптимизация Authorization code grant flow для public-клиентов, которые умеют работать с redirection URI. Например, для браузерных приложений на JavaScript, или мобильных приложений. Требование к user-agent, с помощью которого взаимодействуют клиент и resource owner, сохраняется: он должен уметь работать с HTTP-редиректами.

Между authorization code и implicit есть основное отличие: вместо получения authorization code и access token по нему, мы сразу получаем access token после успешной авторизации resource owner. Кроме того, здесь не используется client secret из соображений безопасности — приложение можно дизассемблировать и получить его. Подлинность проверяется только по redirection URI.



Многие шаги из данной схемы похожи на шаги из authorization code, но предлагаю их разобрать также подробно. Представим, что некое браузерное приложение хочет сохранять свои настройки в нашем Git-репозитории. Мы нажимаете «Войти в GitHub» и на этом этапе начинается работа Implicit flow:

  • Клиент с помощью user-agent и HTTP-редиректа перенаправляет resource owner на сервер авторизации. В параметрах запроса передает client ID и redirection URI, которые нужны для аутентификации клиента и последующего возврата resource owner обратно.
  • Resourse owner аутентифицируется, взаимодействуя через user-agent с сервером авторизации. Заодно подтверждает выдачу grant клиенту, с client ID которого он пришёл.
  • После подтверждения выдачи grant (нажатия «allow» на consent screen), user-agent возвращает resource owner на redirection URI. Кроме того, в URI fragment передаётся access token (URI fragment — это то, что обычно идёт в URI после символа ‘#’).
  • Сам фрагмент сохраняется локально в user-agent. User-agent двигается дальше по redirection URI за web-страницей, которая нужна для получения access token и других необходимых данных из фрагмента. Она может находиться как на самом клиенте, так и на удалённом ресурсе, например, на CDN.
  • Web-ресурс возвращает web-страницу (может содержать в себе скрипт), которая может прочитать полностью redirection URI, в том числе и значение, указанное в фрагменте.
  • User-agent отрисовывает локально полученную страницу, включая исполнение скриптов, которые он получил от web-hosted client resource, которые получают access token.
  • Полученный access token user-agent просто передаёт клиенту.

Это сложный flow. Он мало используется в реальных сценариях. Но его всё ещё можно встретить в legacy-проектах.

Device authorization (RFC 8628)


С 2012 до 2019 появилось много умных устройств, на которых неудобно авторизоваться. Например, неудобно вводить сложный логин и пароль на телевизоре каждый раз при открытии ресурса. На некоторых устройствах это невозможно, например на серверных ОС без графического интерфейса. В августе 2019 этот flow появился как раз для таких сценариев. 

Есть, как минимум, 3 требования к устройствам, чтобы работа с помощью Device authoraztion grant flow была возможна:

  • Устройство должно иметь возможность совершать исходящие HTTPS-запросы.
  • Устройство должно иметь возможность отображать URI и код пользователю.
  • Каждое авторизуемое устройство принадлежит resource owner, который для успешной авторизации должен иметь другое устройство с браузером, чтобы перейти по указанному URI и ввести указанный код.



Возможно, схема кажется сложной из-за обилия стрелок. Разберём её также пошагово, как и разбирали сложные flow до него.

Представим, что мы пытаемся авторизоваться на web-сервисе с помощью телевизора. Мы видим кнопку «Авторизоваться как устройство» и нажимаем. В этот момент начинается наш Device flow:

  • Телевизор делает запрос на сервер авторизации, передавая ему свой client ID.
  • Сервер авторизации убеждается, что такой клиент зарегистрирован и имеет соответствующий тип grant.
  • Если всё хорошо, то Authorization server возвращает device code, user code и verification URI. Device code — это уникальный идентификатор устройства, которое авторизуется в системе.
  • Устройство отображает user code и verification URI владельцу этого устройства — resource owner. Redirection URI может быть передан как строкой, так и с помощью QR-кода — ограничений нет.
  • После того, как устройство отобразило user code и verification URI, оно начинает раз в некоторое время опрашивать сервер авторизации о её успешности.
  • Дальше в бой вступает resource owner. Он переходит по указанному verification URI, аутентифицируется и вводит user code, который он получил от устройства, подтверждая выдачу необходимых scope устройству. На этом действия от имени resource owner закончены.
  • Всё это время устройство (пункт 3) опрашивало сервер авторизации о её успешности. Устройство в очередной раз идёт к серверу авторизации со своим device code и client ID в надежде, что авторизация на этот раз прошла.
  • В этот раз, когда resource owner подтвердил передачу необходимых прав устройству, сервер авторизации возвращает в ответе на запрос access token (если предусмотрено настройками сервера и refresh token). И с помощью токена устройство уже может продолжать работу с ресурсом.

Несмотря на кажущуюся сложность со стрелочками, этот flow тоже достаточно прост. Если вам требуется взаимодействовать с устройствами (а у нас их много: трекер, касса, витрины и прочие устройства), то вам стоит использовать этот flow.

Вместо вывода


В этой статье я опустил много подробностей, чтобы максимально просто и доступно рассказать о самом важном. Например, типы запросов, как и в каком виде передавать параметры, какие символы допустимы в качестве значений для того. 

Если хотите погрузиться в тематику детальнее, то рекомендую в RFC 6749 (для OAuth 2.0) и RFC 8628 (для Device Flow). Кроме того, следить за актуальными версиями RFC можно на ресурсе, посвящённому OAuth.

Если статья была полезна и захотите подробностей — пишите в комментариях, и в следующих статьях расскажу о PKCE, о протоколе аутентификации OpenID Connect 1.0, о нашей реализации сервера аутентификации и многом другом.

Полезные ссылки: