Несколько месяцев назад я занимался реализацией OpenID Connect сервера для управления доступом сотен наших внутренних приложений. От собственных наработок, удобных на меньших масштабах, мы перешли к общепринятому стандарту. Доступ через центральный сервис значительно упрощает монотонные операции, сокращает затраты на реализацию авторизаций, позволяет находить много готовых решений и не ломать голову при разработке новых. В этой статье я расскажу об этом переходе и о шишках, которые мы успели набить.
Несколько лет назад, когда внутренних приложений стало слишком много для ручного управления, мы написали приложение для контроля доступов внутри компании. Это было простое Rails-приложение, которое подключалось к базе данных с информацией о сотрудниках, где настраивался доступ к различному функционалу. Тогда же мы подняли первое SSO, которое основывалось на проверке токенов со стороны клиента и сервера авторизации, токен передавался в шифрованном виде с несколькими параметрами и сверялся на сервере авторизации. Это был не самый удобный вариант так, как на каждом внутреннем приложении нужно было описывать немалый слой логики, а базы сотрудников вовсе синхронизировались с сервером авторизации.
Спустя некоторое время мы решили упростить задачу централизованной авторизации. SSO перевели на балансер. С помощью OpenResty на Lua добавили шаблон, который проверял токены, знал в какое приложение идет запрос и мог проверить, есть ли туда доступ. Такой подход сильно упростил задачу контроля доступов внутренних приложений — в коде каждого приложения уже не нужно было описывать дополнительную логику. В итоге мы закрыли трафик внешне, а само приложение ничего не знало об авторизации.
Однако одна из проблем осталась нерешенной. Как быть с приложениями, которым нужна информация о сотрудниках? Можно было написать API для сервиса авторизации, но тогда пришлось бы добавлять дополнительную логику для каждого такого приложения. К тому же мы хотели избавиться от зависимости от одного нашего самописного приложения, ориентированного в дальнейшем на перевод в OpenSource, от нашего внутреннего сервера авторизации. О нем мы расскажем как-нибудь в другой раз. Решением обеих проблем стал OAuth.
OAuth — это понятный, общепринятый стандарт авторизации, но так как только его функционала недостаточно, рассматривать стали сразу OpenID Connect (OIDC). Сам по себе OIDC — это третья реализация открытого аутентификационного стандарта, которая перетекла в надстройку над протоколом OAuth 2.0 (открытый протокол авторизации). Такое решение закрывает проблему отсутствия данных о конечном пользователе, а также дает возможность смены провайдера авторизации.
Однако мы не стали выбирать конкретного провайдера и решили для нашего существующего сервера авторизации добавить интеграцию с OIDC. В пользу такого решения послужило, то что OIDC очень гибок в плане авторизации конечного пользователя. Таким образом была возможность внедрить поддержку OIDC на своем текущем сервере авторизации.
Для интеграции OIDC необходимо привести текущие данные о пользователях в вид, понятный стандарту. В OIDC это называется Claims. Клеймы по сути — это конечные поля в базе данных о пользователях (name, email, phone и т.д.). Существует стандартный список клеймов, а все, что не входит в этот список, считается кастомным. Поэтому первый момент, на который необходимо обратить внимание, если вы захотите выбрать существующий OIDC-провайдер – возможность удобной кастомизации новых клеймов.
Группа клеймов объединяется в следующее подмножество – Scope. При авторизации идет запрос доступа не к конкретным клеймам, а именно к скоупам, даже если часть клеймов из скоупа не нужна.
Следующей частью интеграции OIDC является выбор и реализация типов авторизации, так называемых грантов. От выбранного гранта будет зависеть дальнейший сценарий взаимодействия выбранного приложения с авторизационным сервером. Примерная схема выбора нужного гранта представлена на рисунке ниже.
Для нашего первого приложения мы использовали самый распространенный грант – Authorization Code. Его отличием от других является то, что он является трехшаговым, т.е. проходит дополнительную проверку. Сначала пользователь делает запрос на разрешение авторизации, получает токен – Authorization Code, затем с этим токеном, словно с билетом на проезд, запрашивает токен доступа. Все основное взаимодействие данного сценария авторизации основано на редиректах между приложением и авторизационным сервером. Подробнее почитать об этом гранте можно здесь.
OAuth придерживается концепции, что токены доступа, полученные после авторизации, должны быть временными и меняться желательно в среднем каждые 10 минут. Грант Authorization Code является трехшаговой проверкой через редиректы, каждые 10 минут такой шаг проворачивать, честно говоря, не самое приятное для глаз занятие. Для решения этой проблемы существует еще один грант – Refresh Token, который мы у себя также задействовали. Тут все проще. Во время проверки с другого гранта, помимо основного токена доступа выдается еще один – Refresh Token, который можно использовать только один раз и время его жизни, как правило, существенно больше. С этим Refresh Token’ом, когда закончится TTL (Time to Live) основного токена доступа, запрос нового токена доступа придет на endpoint уже другого гранта. Использованный Refresh Token сразу обнуляется. Такая проверка является двухшаговой и может быть выполнена в фоне, незаметно для пользователя.
После того, как выбранные гранты реализованы, авторизация работает, стоит упомянуть о получении данных о конечном пользователе. В OIDC есть отдельный endpoint для этого, на котором со своим текущим токеном доступа и при его актуальности можно запросить данные о пользователях. И если данные пользователя не меняются так часто, а ходить за текущими нужно по многу раз, можно прийти к такому решению, как JWT-токены. Эти токены также поддерживаются стандартом. Сам по себе JWT-токен состоит из трех частей: header (информация о токене), payload (любые нужные данные) и signature (подпись, токен подписывается сервером и в дальнейшем можно проверить источник его подписи).
В имплементации OIDC JWT-токен называется id_token. Он может быть запрошен вместе с обычным токеном доступа и все, что остается – проверить подпись. У сервера авторизации для этого существует отдельный endpoint со связкой публичных ключей в формате JWK. И говоря об этом, стоит упомянуть, что существует еще один endpoint, который на основе стандарта RFC5785 отражает текущую конфигурацию OIDC-сервера. В нем содержатся все адреса endpoint’ов (в том, числе адрес связки публичных ключей, используемых для подписи), поддерживаемые клеймы и скоупы, используемые алгоритмы шифрования, поддерживаемые гранты и т.д.
Таким образом с помощью id_token’а можно передавать все нужные клеймы в payload токена и не обращаться каждый раз на сервер авторизации для запроса данных о пользователе. Минусом такого подхода является то, что изменение пользовательских данных от сервера приходит не сразу, а вместе с новым токеном доступа.
Так, после реализации собственного OIDC сервера и настройки подключений к нему на стороне приложений, мы решили проблему передачи информации о пользователях.
Так как OIDC — это открытый стандарт, у нас появилась возможность выбора существующего провайдера или реализации сервера. Мы пробовали Keycloak, который оказался очень удобным в конфигурировании, после настройки и смены конфигураций подключения на стороне приложений он готов к работе. На стороне приложений остается только сменить конфигурации подключения.
В рамках нашей организации в качестве первого OIDC-сервера мы собрали свою реализацию, которая дополнялась по мере необходимости. После подробного рассмотрения других готовых решений, можно сказать, что это спорный момент. В пользу решения реализации своего сервера послужили опасения со стороны провайдеров в отсутствии необходимого функционала, а также наличие старой системы в которой присутствовали разные кастомные авторизации для некоторых сервисов и хранилось уже довольно много данных о сотрудниках. Однако в готовых реализациях, присутствуют удобства для интеграции. Например, в Keycloak своя система менеджмента пользователей и данные хранятся прямо в ней, а перегнать своих пользователей туда не составит большого труда. Для этого в Keycloak есть API, которое позволит в полной мере осуществить все необходимые действия по переносу.
Еще один пример сертифицированной, интересной, на мой взгляд, реализации — Ory Hydra. Интересна она тем, что состоит из разных компонентов. Для интеграции вам понадобится связать свой сервис менеджмента пользователей с их сервисом авторизации и расширять по мере необходимости.
Keycloak и Ory Hydra — не единственные готовые решения. Лучше всего подбирать сертифицированную OpenID Foundation реализацию. Обычно у таких решений есть значок OpenID Certification.
Также не забывайте о существующих платных провайдерах, если вы не хотите держать свой сервер OIDC. На сегодняшний день хороших вариантов много.
В ближайшем будущем мы собираемся закрыть трафик до внутренних сервисов другим способом. Планируем перевести наше текущее SSO на балансере с помощью OpenResty на прокси, в основе которого лежит OAuth. Здесь также существует уже много готовых решений, например:
github.com/bitly/oauth2_proxy
github.com/ory/oathkeeper
github.com/keycloak/keycloak-gatekeeper
jwt.io – хороших сервис для проверки JWT-токенов
openid.net/developers/certified — список сертифицированных реализаций OIDC
Давным-давно… С чего все начиналось
Несколько лет назад, когда внутренних приложений стало слишком много для ручного управления, мы написали приложение для контроля доступов внутри компании. Это было простое Rails-приложение, которое подключалось к базе данных с информацией о сотрудниках, где настраивался доступ к различному функционалу. Тогда же мы подняли первое SSO, которое основывалось на проверке токенов со стороны клиента и сервера авторизации, токен передавался в шифрованном виде с несколькими параметрами и сверялся на сервере авторизации. Это был не самый удобный вариант так, как на каждом внутреннем приложении нужно было описывать немалый слой логики, а базы сотрудников вовсе синхронизировались с сервером авторизации.
Спустя некоторое время мы решили упростить задачу централизованной авторизации. SSO перевели на балансер. С помощью OpenResty на Lua добавили шаблон, который проверял токены, знал в какое приложение идет запрос и мог проверить, есть ли туда доступ. Такой подход сильно упростил задачу контроля доступов внутренних приложений — в коде каждого приложения уже не нужно было описывать дополнительную логику. В итоге мы закрыли трафик внешне, а само приложение ничего не знало об авторизации.
Однако одна из проблем осталась нерешенной. Как быть с приложениями, которым нужна информация о сотрудниках? Можно было написать API для сервиса авторизации, но тогда пришлось бы добавлять дополнительную логику для каждого такого приложения. К тому же мы хотели избавиться от зависимости от одного нашего самописного приложения, ориентированного в дальнейшем на перевод в OpenSource, от нашего внутреннего сервера авторизации. О нем мы расскажем как-нибудь в другой раз. Решением обеих проблем стал OAuth.
К общепринятым стандартам
OAuth — это понятный, общепринятый стандарт авторизации, но так как только его функционала недостаточно, рассматривать стали сразу OpenID Connect (OIDC). Сам по себе OIDC — это третья реализация открытого аутентификационного стандарта, которая перетекла в надстройку над протоколом OAuth 2.0 (открытый протокол авторизации). Такое решение закрывает проблему отсутствия данных о конечном пользователе, а также дает возможность смены провайдера авторизации.
Однако мы не стали выбирать конкретного провайдера и решили для нашего существующего сервера авторизации добавить интеграцию с OIDC. В пользу такого решения послужило, то что OIDC очень гибок в плане авторизации конечного пользователя. Таким образом была возможность внедрить поддержку OIDC на своем текущем сервере авторизации.
Наш путь реализации собственного OIDC-сервера
1) Привели данные в нужный вид
Для интеграции OIDC необходимо привести текущие данные о пользователях в вид, понятный стандарту. В OIDC это называется Claims. Клеймы по сути — это конечные поля в базе данных о пользователях (name, email, phone и т.д.). Существует стандартный список клеймов, а все, что не входит в этот список, считается кастомным. Поэтому первый момент, на который необходимо обратить внимание, если вы захотите выбрать существующий OIDC-провайдер – возможность удобной кастомизации новых клеймов.
Группа клеймов объединяется в следующее подмножество – Scope. При авторизации идет запрос доступа не к конкретным клеймам, а именно к скоупам, даже если часть клеймов из скоупа не нужна.
2) Реализовали нужные гранты
Следующей частью интеграции OIDC является выбор и реализация типов авторизации, так называемых грантов. От выбранного гранта будет зависеть дальнейший сценарий взаимодействия выбранного приложения с авторизационным сервером. Примерная схема выбора нужного гранта представлена на рисунке ниже.
Для нашего первого приложения мы использовали самый распространенный грант – Authorization Code. Его отличием от других является то, что он является трехшаговым, т.е. проходит дополнительную проверку. Сначала пользователь делает запрос на разрешение авторизации, получает токен – Authorization Code, затем с этим токеном, словно с билетом на проезд, запрашивает токен доступа. Все основное взаимодействие данного сценария авторизации основано на редиректах между приложением и авторизационным сервером. Подробнее почитать об этом гранте можно здесь.
OAuth придерживается концепции, что токены доступа, полученные после авторизации, должны быть временными и меняться желательно в среднем каждые 10 минут. Грант Authorization Code является трехшаговой проверкой через редиректы, каждые 10 минут такой шаг проворачивать, честно говоря, не самое приятное для глаз занятие. Для решения этой проблемы существует еще один грант – Refresh Token, который мы у себя также задействовали. Тут все проще. Во время проверки с другого гранта, помимо основного токена доступа выдается еще один – Refresh Token, который можно использовать только один раз и время его жизни, как правило, существенно больше. С этим Refresh Token’ом, когда закончится TTL (Time to Live) основного токена доступа, запрос нового токена доступа придет на endpoint уже другого гранта. Использованный Refresh Token сразу обнуляется. Такая проверка является двухшаговой и может быть выполнена в фоне, незаметно для пользователя.
3) Настроили форматы вывода пользовательских данных
После того, как выбранные гранты реализованы, авторизация работает, стоит упомянуть о получении данных о конечном пользователе. В OIDC есть отдельный endpoint для этого, на котором со своим текущим токеном доступа и при его актуальности можно запросить данные о пользователях. И если данные пользователя не меняются так часто, а ходить за текущими нужно по многу раз, можно прийти к такому решению, как JWT-токены. Эти токены также поддерживаются стандартом. Сам по себе JWT-токен состоит из трех частей: header (информация о токене), payload (любые нужные данные) и signature (подпись, токен подписывается сервером и в дальнейшем можно проверить источник его подписи).
В имплементации OIDC JWT-токен называется id_token. Он может быть запрошен вместе с обычным токеном доступа и все, что остается – проверить подпись. У сервера авторизации для этого существует отдельный endpoint со связкой публичных ключей в формате JWK. И говоря об этом, стоит упомянуть, что существует еще один endpoint, который на основе стандарта RFC5785 отражает текущую конфигурацию OIDC-сервера. В нем содержатся все адреса endpoint’ов (в том, числе адрес связки публичных ключей, используемых для подписи), поддерживаемые клеймы и скоупы, используемые алгоритмы шифрования, поддерживаемые гранты и т.д.
Например в Google:
{
"issuer": "https://accounts.google.com",
"authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
"device_authorization_endpoint": "https://oauth2.googleapis.com/device/code",
"token_endpoint": "https://oauth2.googleapis.com/token",
"userinfo_endpoint": "https://openidconnect.googleapis.com/v1/userinfo",
"revocation_endpoint": "https://oauth2.googleapis.com/revoke",
"jwks_uri": "https://www.googleapis.com/oauth2/v3/certs",
"response_types_supported": [
"code",
"token",
"id_token",
"code token",
"code id_token",
"token id_token",
"code token id_token",
"none"
],
"subject_types_supported": [
"public"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"scopes_supported": [
"openid",
"email",
"profile"
],
"token_endpoint_auth_methods_supported": [
"client_secret_post",
"client_secret_basic"
],
"claims_supported": [
"aud",
"email",
"email_verified",
"exp",
"family_name",
"given_name",
"iat",
"iss",
"locale",
"name",
"picture",
"sub"
],
"code_challenge_methods_supported": [
"plain",
"S256"
],
"grant_types_supported": [
"authorization_code",
"refresh_token",
"urn:ietf:params:oauth:grant-type:device_code",
"urn:ietf:params:oauth:grant-type:jwt-bearer"
]
}
Таким образом с помощью id_token’а можно передавать все нужные клеймы в payload токена и не обращаться каждый раз на сервер авторизации для запроса данных о пользователе. Минусом такого подхода является то, что изменение пользовательских данных от сервера приходит не сразу, а вместе с новым токеном доступа.
Итоги реализации
Так, после реализации собственного OIDC сервера и настройки подключений к нему на стороне приложений, мы решили проблему передачи информации о пользователях.
Так как OIDC — это открытый стандарт, у нас появилась возможность выбора существующего провайдера или реализации сервера. Мы пробовали Keycloak, который оказался очень удобным в конфигурировании, после настройки и смены конфигураций подключения на стороне приложений он готов к работе. На стороне приложений остается только сменить конфигурации подключения.
Говоря о существующих решениях
В рамках нашей организации в качестве первого OIDC-сервера мы собрали свою реализацию, которая дополнялась по мере необходимости. После подробного рассмотрения других готовых решений, можно сказать, что это спорный момент. В пользу решения реализации своего сервера послужили опасения со стороны провайдеров в отсутствии необходимого функционала, а также наличие старой системы в которой присутствовали разные кастомные авторизации для некоторых сервисов и хранилось уже довольно много данных о сотрудниках. Однако в готовых реализациях, присутствуют удобства для интеграции. Например, в Keycloak своя система менеджмента пользователей и данные хранятся прямо в ней, а перегнать своих пользователей туда не составит большого труда. Для этого в Keycloak есть API, которое позволит в полной мере осуществить все необходимые действия по переносу.
Еще один пример сертифицированной, интересной, на мой взгляд, реализации — Ory Hydra. Интересна она тем, что состоит из разных компонентов. Для интеграции вам понадобится связать свой сервис менеджмента пользователей с их сервисом авторизации и расширять по мере необходимости.
Keycloak и Ory Hydra — не единственные готовые решения. Лучше всего подбирать сертифицированную OpenID Foundation реализацию. Обычно у таких решений есть значок OpenID Certification.
Также не забывайте о существующих платных провайдерах, если вы не хотите держать свой сервер OIDC. На сегодняшний день хороших вариантов много.
Что дальше
В ближайшем будущем мы собираемся закрыть трафик до внутренних сервисов другим способом. Планируем перевести наше текущее SSO на балансере с помощью OpenResty на прокси, в основе которого лежит OAuth. Здесь также существует уже много готовых решений, например:
github.com/bitly/oauth2_proxy
github.com/ory/oathkeeper
github.com/keycloak/keycloak-gatekeeper
Дополнительные материалы
jwt.io – хороших сервис для проверки JWT-токенов
openid.net/developers/certified — список сертифицированных реализаций OIDC
y4ppieflu
Есть мнение, что независимо от типа клиента, нужно использовать Authorization Code grant, благо нынче есть расширения типа PKCE, которые позволяют его использовать в мобильных/нативных приложениях и js-приложениях без бэкэнда. Implicit и Password Grant — зло.
И что касается refresh token'а — рекомендовано его выдавать только для confidential clients (т.е. опять же с бэкендом), для public клиентов использовать silent refresh.
g-vit Автор
Спасибо за ваш комментарий, Authorization Code действительно самый надежный и безопасный вариант для публичных клиентов. Implicit, Password Grant больше подойдут для внутренних клиентов. Refresh token может оказаться ненадежным, например в случае возможности внедрения XSS скрипта