Привет! Я Андрей Баронский, бэкенд-тимлид в 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-проектов
К слову, мы делимся не только опытом работы с конкретными технологиями, но и управленческими практиками. Тимлидам и руководителям я предлагаю к прочтению материал нашего управляющего партнера о том, как дать разработчикам свободу при деплое приложений и ускорить процессы в команде.
Удачи!
Автор: baronskiy_a