Вступление

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"
                        }
                    }
                }
            },
            // ...
        ]
    },
    // ...
}

Список всех поддерживаемых провайдеров и документация по тому, как добавить их в проект: тык.

Метрики

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

Трейсинг

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

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

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

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

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

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

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

Комментарии (2)


  1. aleks_raiden
    10.12.2023 07:53

    спасибо огромное, пытался разобраться но неосилил сходу, по вашему мануалу теперь можно снова попробовать


    1. ysomad Автор
      10.12.2023 07:53

      Рад оказалось полезно, на днях еще планирую добавить раздел с поднятием кратоса локально.