Вступление
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"
}
}
}
},
// ...
]
},
// ...
}
Список всех поддерживаемых провайдеров и документация по тому, как добавить их в проект: тык.
Метрики
Все Ory сервисы, включая не только Kratos, имеют метод для получения prometheus метрик: /metrics/prometheus
Трейсинг
Как настроить трейсинг можно найти в документации, на момент написания статьи поддерживается только Jaeger.
Другие проекты Ory
Помимо identity сервиса у Ory есть три решения:
Ory Oathkeeper - zero trust прокси для авторизации входных запросов
Ory Keto - управление правами
Ory Hydra - OpenID Connect/OAuth2 провайдер
Полезные ссылки
Ory Go клиент - клиент ко всем сервисам Ory в одном пакете
aleks_raiden
спасибо огромное, пытался разобраться но неосилил сходу, по вашему мануалу теперь можно снова попробовать
ysomad Автор
Рад оказалось полезно, на днях еще планирую добавить раздел с поднятием кратоса локально.