
Вступление
Ory Kratos - современный cloud native сервер идентификации с поддержкой PassKeys, MFA, FIDO2, TOTP, WebAuthn, с возможностью управления профилями, схемами пользователей, входом через внешние сервисы, регистрацией, восстановлением аккаунта, с поддержкой passwordless входа. Написан на Go, headless, API-first.
Целью этой статьи не является сравнение с другими популярными сервисами идентификации, которые дальше будут упомянуты, но будет рассмотрен основной функционал и мой опыт использования в пет-проектах.
Зачем?
При написании множества пет-проектов всегда остро вставал вопрос авторизации и аутентификации пользователей и как-то раз решил написать свой универсальный сервис идентификации, который устроил бы меня для использования в петах и возможно не только в них.
Со временем пришло осознание что требования к каждому пету достаточно сложно натянуть на модель юзеров текущей реализации такого сервиса (могла быть авторизация по эмейлу, где-то по номеру, а где-то еще и oauth2/oidc хотелось), поэтому присматривался к нескольким готовым решениям, которые покрыли бы мои хотелки. Исходя из этого выделил несколько требований к такому коробочному решению:
возможность развернуть на своем сервере
возможность выбрать идентификатор для пользователей или сделать их несколько (например уникальные адрес эл. почты и номер телефона, по которым можно залогиниться)
возможность безболезненно присобачить OAuth2/OpenID Connect провайдеров, с уже готовыми интеграциями
бесплатно и open source, с активным комьюнити и много звездочек в github (куда же без этого)
возможность создания нескольких типов юзеров, с разным набором полей и методами авторизации
headless, с наличием HTTP/gRPC API
Были рассмотрены:
Firebase, auth0 (платно, нету self hosted решения)
Keycloak (сразу показался огромным динозавром, только зайдя в админку понял что большинство что он предлагает мне не понадобится). Можете кидать в меня камнями за такое непредвзятое решение, но для петов хотелось что-то небольшое и простое
SuperTokens, FusionAuth (нету возможности авторизации по разным идентификаторам)
Ory Kratos. На нем я и остановился, на первый взгляд по документации все хотелки были и проект совсем недавно получил первую мажорную версию, в отличии от других проектов Ory. Другие решения не рассматривал в угоду моего нездорового взгляда на непопуляр��ые решения, буду рад узнать о других от читателей этой статьи.
Браузерное vs нативное API
В Kratos выделили два API для соответствующих приложений:
Браузерное - для приложений, в которых конечный пользователь работает с браузером. При логине будет выдана кука сессии и анти CSRF токен, при получении сессии из браузера (react, vue, angular и.т.д.) с помощью метода
toSession()не будет дополнительных сетевых вызововНативное - для приложений которые общаются с Kratos исключительно по API (также этот вид API должен использоваться для мобильных приложений). При работе с этим API будет выдан токен сессии вместо куки, он эквивалентен куке и при вызове метода
toSession()будет получена та же сессия
Использовать нативное API в браузере не получится физически, Kratos вернет ошибку:
"messages": [ { "id": 4000001, "text": "The HTTP Request Header included the \"Cookie\" key, indicating that this request was made by a Browser. The flow however was initiated as an API request. To prevent potential misuse and mitigate several attack vectors including CSRF, the request has been blocked. Please consult the documentation.", "type": "error" } ]
Модель пользователя (Identity)
Пользователей в Kratos называют Identity, реализующие стандарт JSON schema для описания трейтов (атрибутов), которые можно изменять и дополнять.
У каждой identity теоретически должна быть своя JSON схема, это могут быть как живые пользователи (например можно описать схему для пользователя client и manager), так и системные аккаунты, роботы и.т.д. По умолчанию будет использоваться та схема, которая будет указана в конфиге схемой по умолчанию, и все операции (далее flows) с пользователями будут работать именно с этой схемой (авторизация, регистрация, восстановление и.т.д).
Создать identity со схемой не указанной по умолчанию можно только через админский API с помощью метода POST /admin/identities или с помощью приглашений.
В конфиге установка схемы по умолчанию выглядит следующим образом:
# kratos.yml identity: # будет использоваться по умолчанию для всех flows default_schema_id: user schemas: - id: user # также можно указать схему в base64 или указать url до схемы url: file:///etc/config/kratos/schemas/user.schema.json - id: organizer url: file:///etc/config/kratos/schemas/organizer.schema.json
Трейты (атрибуты пользователя)
Трейты это атрибуты каждой отдельной identity, в каждой JSON схеме должен быть описан как минимум один трейт для идентификатора (например email или username) и один трейт который будет использоваться для верификации и восстановления доступа (верификацию можно отключить в конфиге).
На примере схемы user можно увидеть как описываются трейты, в данной ситуации пользователь имеет один идентификатор - email и авторизацию по паролю (подробнее можно посмотреть в документации):
// user.schema.json { "$id": "user", "$schema": "http://json-schema.org/draft-07/schema#", "title": "User", "type": "object", "properties": { "traits": { "type": "object", "properties": { "email": { "type": "string", "format": "email", // заголовок который можно использовать для отрисовки в GUI "title": "E-Mail", "minLength": 3, "ory.sh/kratos": { "credentials": { "password": { // email является идентификатором для входа с паролем "identifier": true } }, "verification": { "via": "email" }, "recovery": { "via": "email" } } }, "name": { "type": "object", "properties": { "first": { "title": "First Name", "type": "string" }, "last": { "title": "Last Name", "type": "string" } } } }, "required": [ "email" ], "additionalProperties": false } } }
Потоки? (flows)
Это двухэтапные процессы регистрации и управления аккаунтом. В Kratos есть следующие flows:
регистрация
авторизация
выход
настройки профиля (редактирование трейтов)
верификация номера телефона/адреса эл. почты
восстановление
Они делятся на 2 запроса: создание и отправление (submit) flow.
На скриншоте ниже схематично изображен один из flow - регистрация:

При создании flow возвращается список трейтов из схемы пользователя по умолчанию, трейты приходят в поле ui в виде формы, которую нужно отрисовать в интерфейсе (например в браузере), вместе с CSRF токеном и кнопками для входа через внешние сервисы:
{ // ... "type": "browser", "ui": { "action": "http://127.0.0.1:4433/self-service/login?flow=8fb1a5a7-84a5-48ad-8acf-d6b7cc8b2870", "method": "POST", "nodes": [ { "type": "input", "group": "default", "attributes": { "name": "csrf_token", "type": "hidden", "value": "I99Gz/6E81iBy7d82QLknt6MNO/jEw/k8B60l101XbTCIsh5izXLmZV5GiOEU45XEF2JV4FKldMi5+Cyvj7LAA==", "required": true, "disabled": false, "node_type": "input" }, "messages": [], "meta": {} }, { "type": "input", "group": "default", "attributes": { "name": "identifier", "type": "text", "value": "", "required": true, "disabled": false, "node_type": "input" }, "messages": [], "meta": { "label": { "id": 1070004, "text": "ID", "type": "info" } } }, { "type": "input", "group": "password", "attributes": { "name": "password", "type": "password", "required": true, "autocomplete": "current-password", "disabled": false, "node_type": "input" }, "messages": [], "meta": { "label": { "id": 1070001, "text": "Password", "type": "info" } } }, { "type": "input", "group": "password", "attributes": { "name": "method", "type": "submit", "value": "password", "disabled": false, "node_type": "input" }, "messages": [], "meta": { "label": { "id": 1010001, "text": "Sign in", "type": "info", "context": {} } } } ] }, // ... }
Если нужно выполнить что-то во время создания или подтверждения flow, то это можно настроить в конфиге:
selfservice: flows: login: before: # при создании flow hooks: - hook: hook_1 after: # при подтверждении flow hooks: # выполнится для всех методов аутентификации # кроме аутентификации по паролю - hook: hook_2 password: hooks: # выполнится только для аутентификации по паролю - hook: hook_3
Подробнее про хуки можно прочитать тут.
Чтобы получить информацию о сессии из Go можно использовать следующий HTTP middleware:
package httpapi import ( "errors" "fmt" "net/http" "context" kratos "github.com/ory/kratos-client-go" ) func newAuthMiddleware(client *kratos.APIClient) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("ory_kratos_session") if err != nil { w.WriteHeader(http.StatusUnauthorized) return } ctx := r.Context() // Получаем сессию из Kratos session, resp, err := client.FrontendApi. ToSession(ctx). Cookie(cookie.String()). Execute() if err != nil { w.WriteHeader(http.StatusUnauthorized) return } if resp.StatusCode != http.StatusOK { w.WriteHeader(http.StatusUnauthorized) return } if !session.GetActive() { w.WriteHeader(http.StatusUnauthorized) return } identity := session.GetIdentity() // если текущий юзер не соответствует схеме с id = organizer if identity.SchemaId != "organizer" { w.WriteHeader(http.StatusForbidden) return } // положить identity id в контекст для будущего использования ctx = context.WithValue(ctx, "identity_id", identity.Id) r = r.WithContext(ctx) next.ServeHTTP(w, r) }) } }
Если у вас много сервисов и ходить каждый раз по сети становится дорого, можно использовать Ory Oathkeeper как прокси для аутентификации. Прокси будет конвертировать полученную сессию в JWT и далее отправлять его в заголовке, вам остается только вызвать метод sessionToken() в месте где нужно аутентифицировать запрос, передав в него полученный из заголовка токен. Подробнее про Oathkeeper можно почитать в документации.
Вход через внешние сервисы
Kratos из коробки поддерживает вход через внешние сервисы с помощью OAuth2 или OpenID Connect, остается их только сконфигурировать. Далее покажу как подключить вход на примере twitch, для этого нужно в переменную окружения SELFSERVICE_METHODS_OIDC_CONFIG_PROVIDERS записать конфиг в формате JSON массива:
[ { "id": "twitch", "provider": "generic", // взять в ЛК twitch dev.twitch.tv/console "client_id": "CLIENT_ID", "client_secret": "CLIENT_SECRET", "issuer_url": "https://id.twitch.tv/oauth2", // также можно указать в base64 или url на jsonnet "mapper_url": "file:///etc/oidc/oidc.twitch.jsonnet", "scope": [ "openid", "user:read:email" ], "requested_claims": { "id_token": { "email": { "essential": true }, "email_verified": { "essential": true } } } } ]
Чтобы смаппить данные (claims), возвращаемые конкретным провайдером, нужно использовать маппер Jsonnet, путь на маппер указывается в конфиге к провайдеру в поле mapper_url (пример выше). Маппер для twitch:
// oidc.twitch.jsonnet ocal claims = { email_verified: false, } + std.extVar('claims'); { identity: { traits: { // возвращаем адрес эл. почты из полученного объекта claims, //если она подтверждена в twitch [if 'email' in claims && claims.email_verified then 'email' else null]: claims.email, }, }, }
После добавления провайдера в конфиг, для flow авторизации и регистрации, также будут приходить кнопки для входа через интегрированных провайдеров вместе с остальными полями формы:
{ // ... "ui": { "action": "http://127.0.0.1:4433/self-service/login?flow=8fb1a5a7-84a5-48ad-8acf-d6b7cc8b2870", "method": "POST", "nodes": [ { "type": "input", "group": "oidc", "attributes": { "name": "provider", "type": "submit", "value": "twitch", "disabled": false, "node_type": "input" }, "messages": [], "meta": { "label": { "id": 1010002, "text": "Sign in with twitch", "type": "info", "context": { "provider": "twitch" } } } }, // ... ] }, // ... }
Список всех поддерживаемых провайдеров и документация по тому, как добавить их в проект: тык.
Первый запуск
В официальном репозитории Kratos есть несколько конфигураций docker-compose, которые можно использовать для локальной разработки, но в этой статье покажу упрощенный вариант в одном конфиге:
# docker-compose.yml version: '3.7' networks: intranet: volumes: kratos-pg: services: kratos-pg: image: postgres:16.1-alpine volumes: - type: volume source: kratos-pg target: /var/lib/postgresql/data read_only: false ports: - "5432:5432" environment: - POSTGRES_USER=kratos - POSTGRES_PASSWORD=secret - POSTGRES_DB=kratos networks: - intranet kratos-migrate: depends_on: - kratos-pg image: oryd/kratos:1.0.0 env_file: - .env environment: - DSN=postgres://kratos:secret@kratos-pg:5432/kratos?sslmode=disable&max_conns=20&max_idle_conns=4 volumes: - type: volume source: kratos-pg target: /var/lib/postgresql/data read_only: false - type: bind source: ${CONFIG_PATH:?err} # путь до кратос конфига target: /etc/config/kratos/kratos.yml command: -c /etc/config/kratos/kratos.yml migrate sql -e --yes restart: on-failure networks: - intranet kratos: depends_on: - kratos-migrate image: oryd/kratos:1.0.0 ports: - '4433:4433' # порт для публичного API - '4434:4434' # порт для админ API restart: unless-stopped env_file: - .env environment: - DSN=postgres://kratos:secret@kratos-pg:5432/kratos?sslmode=disable&max_conns=20&max_idle_conns=4 command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier volumes: - type: volume source: kratos-pg target: /var/lib/postgresql/data read_only: false - type: bind source: ${CONFIG_PATH:?err} target: /etc/config/kratos/kratos.yml - type: bind source: ./schemas/ # путь до identity схем target: /etc/config/kratos/schemas/ - type: bind source: ./oidc target: /etc/oidc networks: - intranet kratos-selfservice-ui-node: # демо приложение, реализующее все flow image: oryd/kratos-selfservice-ui-node:1.0.0 ports: - "4455:4455" env_file: - .env environment: - PORT=4455 - SECURITY_MODE= networks: - intranet restart: on-failure mailslurper: # smtp сервер для разработки image: oryd/mailslurper:latest-smtps ports: - '4436:4436' - '4437:4437' networks: - intranet
После сборки и запуска всех docker контейнеров на порту 4455 будет доступно демо приложение, реализующее все flow. Формы для flow возвращаются из Kratos с полями identity схем и кнопками для oauth/oidc входа после создания flow:

По умолчанию верификация аккаунта включена по email. На примере flow регистрации должно приходить соответствующее письмо, его можно посмотреть в интерфейсе mailslurper на порту 4436:

Метрики
Все Ory сервисы, включая не только Kratos, имеют метод для получения prometheus метрик: /metrics/prometheus
Трейсинг
Как настроить трейсинг можно найти в документации, на момент написания статьи поддерживается только Jaeger.
Другие проекты Ory
Помимо identity сервиса у Ory есть три решения:
Ory Oathkeeper - zero trust прокси для авторизации входных запросов
Ory Keto - управление правами
Ory Hydra - OpenID Connect/OAuth2 провайдер
Полезные ссылки
Ory Go клиент - клиент ко всем сервисам Ory в одном пакете