Ory Kratos — коробочный SSO

от автора

Вступление

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 — регистрация:

Sign Up flow

Sign Up 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:

Flow регистрации

Flow регистрации

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

Интерфейс mailslurper

Интерфейс mailslurper

Метрики

Все Ory сервисы, включая не только Kratos, имеют метод для получения prometheus метрик: /metrics/prometheus

Трейсинг

Как настроить трейсинг можно найти в документации, на момент написания статьи поддерживается только Jaeger.

Другие проекты Ory

Помимо identity сервиса у Ory есть три решения:

  • Ory Oathkeeper — zero trust прокси для авторизации входных запросов

  • Ory Keto — управление правами

  • Ory Hydra — OpenID Connect/OAuth2 провайдер

Полезные ссылки


ссылка на оригинал статьи https://habr.com/ru/articles/779534/