Привет! Я Андрей Баронский, бэкенд-тимлид в KTS.
Одно из ключевых направлений деятельности нашей компании — это аутсорс-разработка цифровых продуктов. При создании очередной системы мы хотим уделять больше времени и сил необходимым фичам для клиентов, а не настройке рутинного взаимодействия с юзерами, и для ускорения проработки основных пользовательских сценариев мы используем технологию Ory Kratos. В статье я расскажу, почему я рекомендую обратить на нее внимание и как с ней работать.
Для тех, кто впервые сталкивается с этим названием, дам немного контекста. Ory Kratos — это система API-first Identity и User Management. Она управляет всеми аспектами работы с пользователями, включая регистрацию, вход, восстановление пароля, многофакторную аутентификацию, верификацию данных и управление профилем.
Иными словами, Ory Kratos берёт на себя рутинные технические задачи, предлагая готовое, гибкое и удобное в интеграции решение.
Оглавление
Сразу оговорюсь, что на Хабре уже есть статья, посвященная Kratos, и в ней довольно подробно разобрана основная функциональность технологии. Чтобы не повторяться, я не буду заострять внимание на деталях, описанных там. В своем материале я постараюсь больше сосредоточиться на опыте использования и показать, как можно на практике прикрутить Kratos к проекту.
Общая информация
Краткое описание
Ory Kratos распространяется под лицензией Apache-2.0, что даёт свободу его использования, модификации и распространения. Написанный на языке Go, он отличается высокой производительностью, стабильностью и масштабируемостью. Продукт активно развивается, регулярно получая обновления и новые функции. Простота внедрения и отличная документация делают его хорошим выбором для разработчиков.
Разумеется, существуют и альтернативные SSO, но мы отказались от них по своим причинам.
Мы рассматривали Keycloak (24к звезд на гитхабе) и CAS (11к звезд), однако они нам не подошли, поскольку они написаны на Java и довольно неповоротливы в кастомизации, а ее выполнение требует большого и неудобного стека. К примеру, у Keycloak нет плагина для работы с OTP, и его пришлось бы дописывать самостоятельно.
Также мы смотрели в сторону Authelia (22к звезд) — как и Kratos (11,4к звеад), она написана на Go, и, в целом, тоже подходит под наши задачи. Но в итоге наш выбор пал на Kratos ввиду его простоты, исчерпывающей документации с примерами и возможностями кастомизации UI через разработку.
Почему Ory Kratos: еще несколько причин
Давайте рассмотрим остальные факторы, которые определили наш выбор:
наш собственный опыт — ранее мы уже экспериментировали с проектами Ory;
наш стандартный технологический стек — мы понимали, что можем без затруднений доработать приложение под собственные нужды;
возможность легко кастомизировать клиентскую часть приложения Kratos;
наличие нативной авторизации через клиентский API;
синергия Kratos с Hydra (провайдер OAuth 2.0 и ID Connect) и Oathkeeper (API gateway), которые имеют готовые компоненты для интеграции друг с другом без каких-либо доработок. Об этих технологиях я расскажу в следующих статьях;
удобная и понятная документация;
готовые Helm-чарты для развертывания приложения на k8s-кластере;
скорость, с которой можно было поднять пилотную MVP версию приложения;
легкость и изолируемость компонентов при дальнейшей замене.
Как работать с Ory Kratos
Верхнеуровневый обзор
Перед тем как перейти к подробному разбору конфигурации Ory Kratos, давайте обозначим сценарий, на котором будет строиться дальнейшее описание. Мы будем рассматривать стандартное приложение, которое включает:
SSO (Single Sign-On);
веб- и мобильный клиенты для взаимодействия с пользователем;
интеграцию с сервисом рассылок (например, для подтверждения email или отправки уведомлений);
возможную интеграцию с внешним SSO для обеспечения единой точки входа в приложение;
отдельный бизнес-слой, реализующий специфические сценарии приложения.
Для наглядности рассмотрим архитектурную схему, которая поможет структурировать процесс настройки и конфигурации. Опираясь на нее, мы шаг за шагом разберем основные этапы настройки Ory Kratos для работы в таком окружении. Это позволит на практике увидеть, как гибкость системы помогает адаптировать её под разнообразные бизнес-требования и технические сценарии.
[BACKEND] Как собирать бэкенд
Для начала нужно определиться, в каких сценариях будет применяться ваша система. Следует ли предусмотреть возможность регистрации, или это будет закрытая система для сотрудников? Можно ли менять информацию о пользователе, или она пробрасывается автоматически через интеграцию с другой мастер-системой?
Когда мы знаем ответы на эти вопросы, мы можем описать в рамках приложения, в каких сценариях (flow) система будет использоваться. Всего на выбор Kratos дает шесть сценариев:
settings (изменение пользовательских данных);
recovery (восстановление доступов);
verification (подтверждение пользователя);
logout;
login;
registration.
При необходимости ненужные сценарии можно отключить. К примеру, если вы работаете над внутренней системой для сотрудников, то сценарий регистрации вам не понадобится. Это можно указать через конфиг:
kratos.config
selfservice:
flows:
verification:
enabled: false
Пока что в качестве примера мы рассматриваем клиентское приложение, в котором не используются верификация и восстановление доступов.
Примечание: кроме обмена данными между клиентом и сервером, backend-часть приложения выполняет редиректы. Для этого нужно указать путь до соответствующих разделов веб-приложения:
kratos.config
selfservice:
default_browser_return_url: http://127.0.0.1:4455/welcome
flows:
login:
ui_url: http://127.0.0.1:4455/login
Когда вы определились, какие сценарии вам понадобятся, нужно сформулировать, как будет происходить аутентификация пользователя в системе. Рассмотрим ее на примере «email+пароль». Нужно указать это в конфиге следующим образом:
kratos.config
...
selfservice:
methods:
password:
enabled: true
...
Система также предлагает альтернативные методы аутентификации — они могут пригодиться, если вам не подходит вариант «логин+пароль»:
Passwordless + OTP/Magic Links;
Passkeys и WebAuthN;
Multi-factor authentication;
Social sign in.
Далее необходимо сконфигурировать криптографию и шифрование системы. Внимание на этом заострять не буду, просто приведу пример, как это может выглядеть в конфиге:
kratos.config
secrets:
cookie:
- PLEASE-CHANGE-ME-I-AM-VERY-INSECURE
cipher:
- 32-LONG-SECRET-NOT-SECURE-AT-ALL
ciphers:
algorithm: xchacha20-poly1305
hashers:
algorithm: bcrypt
bcrypt:
cost: 8
К слову, длительность жизни сессий и OTP гибко настраиваются. Их также можно задать через конфиг.
У бэкенда приложения есть Public и Admin API. В конфиге также нужно указать соответствующие им адреса:
kratos.config
serve:
public:
base_url: http://kratos:4433/
admin:
base_url: http://kratos:4434/
Когда все предыдущие шаги выполнены, остается только сконфигурировать интеграцию с СУБД. Приложение поддерживает работу с реляционными базами данных (PostgreSQL, MySQL, SQLite и CockroachDB). Для ознакомления также доступна возможность работы в режиме in memory:
kratos.config
dsn: memory
Подробнее о сборке бэкенда см.:
[DB] Как собирать юзера для базы данных
В Kratos пользователь — это ключевая сущность. Описать его можно с помощью документа JSON Schema. Для тех, кто раньше не сталкивался, JSON Schema — это декларативный язык для определения структуры и ограничений JSON.
Нам нужно описать основные атрибуты пользователя, указать идентификатор и способ логина. Это делается с помощью расширенных атрибутов ory.sh/kratos, в которых указываются системные зависимости.
Стоит обратить внимание, что title атрибутов пользователя будут использоваться в UI клиентского приложения. Еще при конфигурировании юзера важно помнить, что сервис Kratos отвечает только за работу с пользователем. Следовательно, в его атрибутах не нужно хранить какую-то специфическую бизнес логику, не относящуюся к домену. Храните только те данные, которые будут необходимы во всех компонентах системы.
Рассмотрим конфигурацию на примере:
identity.schema.json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"username": {
"title": "Username",
"type": "string",
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
}
}
},
"name": {
"type": "object",
"properties": {
"first": {
"title": "First name",
"type": "string"
},
"last": {
"title": "Last name",
"type": "string"
}
}
}
}
}
}
}
Кроме указанных выше атрибутов, есть еще JSON-атрибуты metadata_public (доступные клиенту данные) и metadata_admin (данные, которые можно получить на уровне сервера). За консистентность данных нужно будет отвечать самостоятельно. Валидации этих параметров нет.
После того, как мы разобрались с «анатомией» пользователя, в конфиге сервера нужно указать путь до схемы.
kratos.config
identity:
default_schema_id: default
schemas:
- id: default
url: file:///etc/config/kratos/identity.schema.json
Подробнее см.:
-
Документация:
[FRONTEND] Как собирать фронтенд
Для реализации своего собственного пользовательского интерфейса есть 3 варианта репозиториев, но основе которых можно это сделать:
В данном примере мы будем использовать Next.js/React-приложение из репозитория. Я предлагаю рассмотреть, как указать переменные окружения для конфигурирования фронтенда. В качестве примера возьмем демо веб-приложение:
environment:
- KRATOS_PUBLIC_URL=http://kratos:4433/
- KRATOS_BROWSER_URL=http://127.0.0.1:4433/
- COOKIE_SECRET=changeme
- CSRF_COOKIE_NAME=ory_csrf_ui
- CSRF_COOKIE_SECRET=changeme
Визуализация работы (preview)
Посмотрим, какого результата можно достичь текущей реализацией. На видео используется максимально приближенный к описанному конфиг:
Схема работы (исходник здесь):
Углубленный обзор
Мы познакомились с базовыми сценариями IdM-решения, и теперь я предлагаю рассмотреть дополнительные возможности инструмента, которые могут пригодиться при интеграции Kratos в проект.
Интеграция с сервисом рассылок
Перед тем, как интегрировать наше приложение с сервисом рассылок, нужно добавить необходимость подтвердить почту в сервисе.
1. Добавляем шаг подтверждения почты во flow верификации:
kratos.config
flows:
verification:
enabled: true
identity.schema.json (строки 16, 17):
"properties": {
"traits": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "E-Mail",
"minLength": 3,
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
},
"verification": {
"via": "email"
}
}
}
}
}
}
2. Конфигурируем интеграцию с сервисом рассылок. Это может быть реализовано с помощью протокола SMTP. Для того, чтобы уменьшить объем кода и избыточного контекста, я опущу детали работы с аутентификацией. Отмечу только, что возможности ее настройки в проекте есть.
kratos.config
courier:
smtp:
connection_uri: smtps://test:test@mailslurper:1025
Также в Kratos есть возможность шаблонизации сообщений через Go template engine. Если вы с ними ранее не сталкивались с этим инструментом, тут можно с ним ознакомиться. В этом документе указаны переменные, которые будут переброшены при генерации тела письма.
email.body.gotmpl
Hi, please verify your account by code:
{{ .VerificationCode }}
Если же интеграция с сервисом рассылок через SMTP вам не подходит, у вас свой сервер или вы пользуетесь готовым решением, где работа с шаблонами писем уже реализована с интеграцией через HTTP API, то в системе также предусмотрена альтернативная интеграция через HTTP.
kratos.config
...
courier:
delivery_strategy: http
http:
request_config:
url: https://api.sendsrv.com/mail/send
method: POST
body: file:///etc/config/kratos/mail.template.jsonnet
headers:
"Content-Type": "application/json"
...
В случае HTTP-интеграции тело запроса шаблонизируется с помощью конфигурационного языка jsonnet. Аналогично при генерации тела запроса будет передан ctx, из которого можно будет достать необходимые переменные и данные о пользователе. Посмотрим на примере интеграции с сервисом рассылок Unisender:
http_courier_template
function(ctx) {
message: {
recipients: [
{
email: ctx.recipient,
substitutions: {
RECOVERY_CODE: ctx.template_data.recovery_code,
},
},
],
from_email: "no-reply@yourapp.ru",
template_id: "00000000-0000-0000-0000-000000000000"
}
}
В итоге получаем следующий результат:
Подробнее см. документацию:
Пара слов про API-авторизацию (native/browser)
Так как помимо обмена данных Kratos выполняет редиректы, у продукта есть отдельный API для native-клиентов. Разработчики рекомендуют воздержаться от использования native-клиентов в браузерных приложениях, поскольку это это открывает брешь в обороне для CSRF-атак.
Webhooks и как их использовать
В продукте есть возможность кастомизации логики с помощью внешних интеграций через webhook. Для всех основных flow есть хуки before и after, которые позволяют реализовать все необходимые сценации.
При описании webhook есть возможность сконфигурировать их поведение: до или после выполнения сценария, блокирующий или нет. Это может пригодиться, например, если после успешной регистрации пользователя вам нужно автоматически добавлять его в списки рассылок в отдельном сервисе. Или, наоборот, усложнить регистрацию в систему, добавив дополнительные шаги проверок в других компонентах. Тело запроса описывается аналогично примеру с HTTP-интеграцией рассылок с помощью Jsonnet.
Более того, в Kratos даже есть возможность обновлять атрибуты identity через webhook — как UI-параметры пользователя, так и metadata (публичные и приватные).
Рассмотрим ситуацию, при которой мы хотим дополнительно проверить заявку на регистрацию пользователя в другом компоненте нашей системы. Для этого воспользуемся блокирующим after-хуком с парсингом тела ответа, чтобы заводить пользователей в систему только после прохождения проверки.
Расширим конфиг сервиса настройками flow регистрации:
kratos.config
selfservice:
flows:
registration:
after:
password:
hooks:
- hook: web_hook
config:
url: http://wiremock:8443/webhook
method: "POST"
body: "file:///etc/config/kratos/reg_webhook.jsonnet"
response:
parse: true
Приведем пример ответа, который будет обрабатывать Kratos и информировать о проблеме с регистрацией, и замокаем ответ на вебхук. В данном случае "instance_ptr": "#/traits/email"
(4 строка) является атрибутом, с которым ассоциируется ошибка:
{
"messages": [
{
"instance_ptr": "#/traits/email",
"messages": [
{
"id": 12356,
"text": "Мы решили не пускать тебя в систему!(",
"type": "error",
"context": {
"value": "Мы решили не пускать тебя в систему!("
}
}
]
}
]
}
На видео можно посмотреть пример блокирующего вебхука c информацией об ошибке в UI:
Подробнее см. документацию:
Получение пользовательской информации в сервисах бизнес-логики
Можно, конечно, реализовать мидлвар в наших микросервисах для получения информации о пользователях, но я предлагаю использовать для этого Oauthkeeper — AGW, за которым можно скрыть все компоненты приложения. В нем же можно сконфигурировать, какие пользовательские данные мы будем передавать в микросервисы. Так мы избавляемся от необходимости дублировать интеграцию с Kratos в каждом сервисе и можем легко настраивать это на стороне AGW.
OAUTH2
Стоит отметить, что Ory Kratos из коробки поддерживает вход через внешние сервисы с помощью OAuth2 и OpenID Connect. Вам остается только сконфигурировать их. Не буду погружаться в детали, просто оставлю ссылку на соответствующий раздел документации, если вам интересно узнать об этом больше.
Аутентификация по нескольким id
В Kratos есть возможность настроить несколько identifiers и указать различные варианты аутентификации для них. Зачем это нужно? Например, для того, чтобы пользователь мог логиниться в системе не только по адресу электронной почты, но и по номеру телефона. Рассмотрим этот пример подробнее.
Примечание: здесь я не буду рассматривать механизм инкрементального обновления пользовательской схемы через миграции.
Выше мы уже добавили возможность входить в наше приложение через связку «email + password». Добавим также связку «phone + password». Для этого нужно расширить схему пользователя. Доработаем ее:
Identity.schema.json
{
"$id": "https://schemas.ory.sh/presets/kratos/identity.email.schema.json",
"title": "Person",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"phone": {
"type": "string",
"format": "tel",
"title": "Phone number",
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true,
}
}
}
}
}
}
}
}
Приятные мелочи
Отдельно хочу подсветить небольшие детали, которые нередко упрощают жизнь при работе с Kratos.
Во-первых, в системе есть возможность запускать Cleanup jobs, чтобы чистить устаревшие сессии и освобождать данные с диска. Подробнее про них можно почитать в документации.
Во-вторых, разработчики добавили возможность деактивации пользователей. Здесь тоже оставлю ссылку на соответствующий раздел.
В-третьих, в Kratos предусмотрена миграция пользователей через import, причем при миграции можно сохранять их прежние пароли. По традиции, детали в документации.
Можно задать ограничение на уровне конфига — не более одной активной сессии на пользователя.
К слову об активной поддержке и расширении функциональности: пока я писал эту статью, появилась возможность реализовать passwordless flow для логина и регистрации через sms.
Небольшое (ленивое) нагрузочное
Предлагаю посмотреть, как Kratos справляется с нагрузками. Здесь я не преследую цели провести полноценное испытание в условиях, максимально приближенных к реальным, а просто тестирую технологию «в вакууме», чтобы проверить основные паттерны.
Проверим систему двумя простыми сценариями. Тестировать будем следующую конфигурацию:
1 запущенный инстанс приложения на k8s-кластере;
порты проброшены на локальную машину;
лимиты по памяти — 512 MB.
Первый сценарий нагрузки
N пользователей (в нашем случае — 50) проходят flow аутентификации из двух шагов: создание flow и отправка данных (логин + пароль).. Задержка между шагами — от одной до трех секунд. Разумеется, в действительности типовая нагрузка будет не такой, но сценарий дает нам представление о том, как сервис будет справляться с наплывом новых пользователей в начале своей работы.
Получаем следующие результаты:
С заданной конфигурацией Kratos успевает обрабатывать чуть больше 20 запросов в секунду.
Больше половины полученных запросов на пике обрабатываются быстрее, чем за две секунды; 95 % запросов обрабатываются быстрее, чем за четыре секунды.
Второй сценарий нагрузки
Пользователь проходит flow аутентификации и начинает в цикле запрашивать информацию о профиле. Также этот запрос может использовать API Gateway для проверки сессии пользователя. Именно такой и будет основная нагрузка на сервис.
Получаем следующие результаты:
Как и предполагалось, пик нагрузки происходит в момент, когда пользователи активно логинятся: 100 одновременных юзеров входят в систему примерно за 20 секунд, на протяжении которых RPS не поднимается выше 100.
После того, как все пользователи получают активную сессию, проходит пик нагрузки. При 100 пользователях Response Time для 95% запросов не превышает 400 мс. RPS при этом держится чуть меньше 600.
Минимальное время обработки запроса — 30 мс.
Выводы
Судя по результатам, «узкое горлышко» системы — это момент активного наплыва пользователей, которые одновременно хотят залогиниться. С запросами информации о профиле Kratos справляется намного эффективнее при прочих равных. Горизонтальное масштабирование системы (увеличение количества инстансов) может помочь решить эту проблему в критические моменты.
Недостатки Kratos
Разумеется, в работе мы столкнулись и с негативными аспектами системы. Поговорим про некоторые из них.
Нет ограничений на отправку кода верификации и восстановления доступов, и сделано это не будет. Нужно самостоятельно решать вопрос с ограничениями на стороне AGW и других интеграций.
Нет готовой admin-панели. Ее не очень сложно реализовать самостоятельно с помощью кодогенерации поверх схемы базы данных; к примеру, мы используем django + sdk для интеграции с API. Однако отсутствие готовой панели все же несколько огорчает.
Заблокированным пользователям может быть неочевидно, что они заблокированы. Они могут начать восстанавливать пароль или попробовать залогиниться в систему с помощью OTP, получить код от системы, и только при его вводе узнать о блокировке. Можно попробовать это решить с помощью блокирующего before-вебхука с парсингом ответа.
Границы применимости и заключение
Мы испытали Kratos на нескольких проектах и сформулировали ряд выводов, касающихся удобства системы. Я смело рекомендую пользоваться этой технологией, если:
вы не хотите тратить время разработчиков на создание своих кастомных велосипедов и согласны пользоваться сценариями, предложенными системой (или готовы вводить кардинальные изменения системы через fork проекта);
вас устраивает заложенный UI готового решения;
вы готовы завести отдельное реляционное хранилище пользователей и можете синхронизировать его с мастер-данными;
вам не нужна интеграция с AD (если нужна, то решение вам не подойдет).
На случай, если система вам интересна и вы захотели протестировать ее самостоятельно, оставляю ссылки на полезные материалы:
Туториал по быстрому старту (и соответствующий раздел репозитория на GitHub)
Структура валидатора конфига (берите конфиг из Quickstart и уже дальше докручивайте при необходимости)
Helm chart, который можно использовать для начала
Если у вас остались вопросы или какой-то из разделов показался не до конца раскрытым, то приходите в комментарии, с радостью обсудим.
Более конвенциональным разработчикам, которые используют Keycloak, я рекомендую почитать статью моего коллеги из DevOps-юнита о том, как прикрутить к нему Firezone. Также советую ознакомиться с материалами из нашего блога для бэкендеров и DevOps-инженеров:
JupyterHub на стероидах: реализация KubeFlow фич без масштабных интеграций
Поднимаем динамические окружения (фича-стенды) для stateless- и stateful-сервисов
В чем силиум, брат? Обзор ключевых фишек Cilium и его преимущества на фоне других CNI-проектов
К слову, мы делимся не только опытом работы с конкретными технологиями, но и управленческими практиками. Тимлидам и руководителям я предлагаю к прочтению материал нашего управляющего партнера о том, как дать разработчикам свободу при деплое приложений и ускорить процессы в команде.
Удачи!
qbertych
Ого, какой изумруд под конец года! Похоже пока что это бесспорный лидер по соотношению количества лайков к количеству просмотров ;)