Перейти к содержанию

Auth - Zitadel (Аутентификация и авторизация)

Общее описание

Сервер аутентификации на базе Zitadel — self-hosted решения для управления идентификацией и доступом, поддерживающего OIDC/OAuth 2.0. Обеспечивает единую систему аутентификации для всей платформы AACSearch.

Расположение: /root/auth Сервер: 185.130.225.35 Домен: auth.aacsearch.com Локальный доступ: localhost:8090 (требует заголовок Host: auth.aacsearch.com)

Структура каталога

/root/auth/
├── docker-compose.yml          # Docker Compose стек Zitadel
├── admin.pat                    # PAT администратора (IAM_OWNER)
├── login-client.pat             # PAT клиента логина (IAM_LOGIN_CLIENT)
├── .masterkey                   # Мастер-ключ шифрования Zitadel
└── zitadel-action-plan-code.js  # Zitadel Action для инжекции plan_code

Docker Compose стек

Сервисы

1. Zitadel (Основной сервис)

Параметр Значение
Образ ghcr.io/zitadel/zitadel:latest
Порт 127.0.0.1:8090:8080 (Admin/API)
Порт 127.0.0.1:3100:3000 (User info)
Restart unless-stopped
Health Check /app/zitadel ready каждые 10 сек

Запуск: start-from-init --masterkey "6AwsIimPLSsujwL0jOC2USqSs7zI7dUN"

Конфигурация (через переменные окружения):

# Внешний домен
ZITADEL_EXTERNALDOMAIN: auth.aacsearch.com
ZITADEL_EXTERNALPORT: 443
ZITADEL_EXTERNALSECURE: "true"
ZITADEL_TLS_ENABLED: "false"  # TLS терминируется на reverse proxy

# База данных
ZITADEL_DATABASE_POSTGRES_HOST: db
ZITADEL_DATABASE_POSTGRES_PORT: 5432
ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel
ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel
ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: zitadel
ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable
ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: postgres
ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: postgres
ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable

# Login V2
ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2: "true"
ZITADEL_DEFAULTINSTANCE_FEATURES_ACTIONS_V2: "true"
ZITADEL_DEFAULTINSTANCE_LOGINV2_BASEURI: "https://auth.aacsearch.com/ui/v2/login"

2. Zitadel Login V2

Параметр Значение
Образ ghcr.io/zitadel/zitadel-login:latest
Сеть service:zitadel (общая сеть с Zitadel)
PAT /current-dir/login-client.pat
Restart unless-stopped

3. PostgreSQL

Параметр Значение
Образ postgres:17-alpine
Порт 127.0.0.1:5433:5432
БД zitadel
Пользователь postgres:postgres
App-пользователь zitadel:zitadel
Хранилище zitadel_pg_data (persistent volume)

Сеть

Все сервисы объединены в изолированную bridge-сеть zitadel.

Порядок запуска

PostgreSQL (health check: pg_isready)
    └─► Zitadel (health check: /app/zitadel ready)
         └─► Zitadel Login V2

Учётные данные и ключи

Admin PAT (файл: admin.pat)

vlvmi5TFQ8NgMv1M_I30ssnZISeM0HJ_vHyAN3TkAnYfQSZbDYcnzzf2DH--XmdMOrW3BxU
  • Пользователь: admin-service
  • Роль: IAM_OWNER
  • Назначение: Управление Zitadel через Management API — создание организаций, пользователей, привязка Action, работа с метаданными

Login Client PAT (файл: login-client.pat)

h1OOFd4zha-gjinks9sveahGkeUJdDRLt7hVmqAJjAe2Ujax8Xl53o7RG8d09GIQAnMK_EA
  • Пользователь: login-client
  • Роль: IAM_LOGIN_CLIENT
  • Назначение: Аутентификация сервиса zitadel-login
  • Срок действия: до 2030-01-01

Master Key (файл: .masterkey)

6AwsIimPLSsujwL0jOC2USqSs7zI7dUN

Используется для шифрования конфиденциальных данных в БД Zitadel.


Zitadel Action — inject-plan-code

Файл: zitadel-action-plan-code.js ID Action: 359305781885009923 Flow: 2 (Complement Token) Triggers: 4 (Pre Userinfo creation), 5 (Pre Access Token creation)

Исходный код

function complementToken(ctx, api) {
  var metadata = ctx.v1.org.getMetadata();
  if (metadata === undefined || metadata.count === 0) {
    return;
  }
  metadata.metadata.forEach(function(md) {
    if (md.key === 'plan_code' && md.value) {
      api.v1.claims.setClaim('plan_code', md.value);
    }
  });
}

Назначение

При создании access token или userinfo response Zitadel: 1. Вызывает эту функцию 2. Она читает метаданные организации 3. Если найден ключ plan_code, добавляет его как custom claim в JWT 4. API-шлюз использует этот claim для определения тарифного плана тенанта

Привязка к триггерам

Привязка выполнена через Management API Zitadel:

# Flow 2, Trigger 4 (Pre Userinfo)
curl -X POST "https://auth.aacsearch.com/management/v1/flows/2/trigger/4" \
  -H "Authorization: Bearer $(cat /root/auth/admin.pat)" \
  -H "Content-Type: application/json" \
  -d '{"actionIds": ["359305781885009923"]}'

# Flow 2, Trigger 5 (Pre Access Token)
curl -X POST "https://auth.aacsearch.com/management/v1/flows/2/trigger/5" \
  -H "Authorization: Bearer $(cat /root/auth/admin.pat)" \
  -H "Content-Type: application/json" \
  -d '{"actionIds": ["359305781885009923"]}'

ВАЖНО: Используется actionIds (массив), НЕ actionId (единственное число)!


Потоки аутентификации

OAuth 2.0 / OIDC (Authorization Code Flow)

1. Клиент → GET /oauth/v2/authorize
   (response_type=code, client_id, redirect_uri, scope)
2. Zitadel Login V2 UI → /ui/v2/login?authRequest=<encoded>
   (пользователь вводит логин/пароль)
3. Zitadel Action "inject-plan-code" выполняется:
   - Trigger 5: Pre Access Token creation
   - Читает org metadata → добавляет plan_code в JWT claims
4. Redirect → redirect_uri?code=<auth_code>
5. Клиент → POST /oauth/v2/token
   (grant_type=authorization_code, code)
6. Zitadel возвращает access_token + id_token + refresh_token
   JWT содержит: sub, org_id, plan_code, ...

JWT Bearer (Machine Users)

1. Machine User генерирует JWT с:
   - iss: client_id
   - sub: user_id (machine user)
   - aud: https://auth.aacsearch.com
   - iat, exp
   - Подписан RSA private key

2. POST /oauth/v2/token
   grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
   assertion=<signed_jwt>
   scope=openid urn:zitadel:iam:user:resourceowner

3. Zitadel возвращает access_token

ВАЖНО: Actions complement_token НЕ выполняются для jwt-bearer grant (machine users)! Только для authorization_code flow. Поэтому plan_code в JWT machine user'ов отсутствует — API-шлюз должен получать его из KV-кеша.

Необходимые scope для получения tenantId в JWT

openid urn:zitadel:iam:user:resourceowner

Scope urn:zitadel:iam:user:resourceowner добавляет в JWT claim urn:zitadel:iam:user:resourceowner:id, который используется API-шлюзом как tenantId.


Метаданные организации

Установка plan_code

curl -X POST "https://auth.aacsearch.com/management/v1/metadata/plan_code" \
  -H "Authorization: Bearer $(cat /root/auth/admin.pat)" \
  -H "x-zitadel-orgid: <ORG_ID>" \
  -H "Content-Type: application/json" \
  -d '{"value": "<base64(plan_code)>"}'

ВАЖНО: Значение метаданных передаётся в base64-кодировке!

Удаление plan_code

curl -X DELETE "https://auth.aacsearch.com/management/v1/metadata/plan_code" \
  -H "Authorization: Bearer $(cat /root/auth/admin.pat)" \
  -H "x-zitadel-orgid: <ORG_ID>"

Чтение метаданных в Action

var metadata = ctx.v1.org.getMetadata();
// Возвращает: { count: N, metadata: [{key: "plan_code", value: "base64_value"}] }

Интеграция с API-шлюзом

JWKS (JSON Web Key Set)

API-шлюз верифицирует JWT через JWKS: - URL: https://auth.aacsearch.com/oauth/v2/keys - Кеш: KV ключ zitadel:jwks (TTL 1 час)

OIDC Configuration

Параметр Значение
Issuer https://auth.aacsearch.com
Client ID 359289240758059011
Authorization Endpoint https://auth.aacsearch.com/oauth/v2/authorize
Token Endpoint https://auth.aacsearch.com/oauth/v2/token
JWKS URI https://auth.aacsearch.com/oauth/v2/keys
Userinfo Endpoint https://auth.aacsearch.com/oidc/v1/userinfo

Интеграция с биллингом (Lago)

Двусторонняя синхронизация plan_code между Lago и Zitadel:

Lago Webhook (subscription.started/updated)
API-шлюз обрабатывает вебхук
POST /management/v1/metadata/plan_code
    (с x-zitadel-orgid и base64(plan_code))
Zitadel сохраняет plan_code в метаданных организации
Следующий вызов complementToken()
    добавляет plan_code в JWT

При отмене подписки (subscription.terminated): - DELETE /management/v1/metadata/plan_code - plan_code удаляется из метаданных организации


Тестовые организации

Professional (тестовый тенант)

Параметр Значение
Org ID 359306499614310403
Machine User ID 359306552479318019
Service Key /tmp/e2e-service-key.json
JWT скрипт /tmp/get-jwt.mjs

Starter (тестовый тенант)

Параметр Значение
Org ID 359312301376929795
Machine User ID 359312311728472067
Service Key /tmp/starter-test-key.json
JWT скрипт /tmp/get-starter-jwt.mjs

Сетевая конфигурация

TLS

  • TLS в контейнере: отключён (ZITADEL_TLS_ENABLED: false)
  • TLS терминация: на внешнем reverse proxy (Nginx на хосте)
  • Reverse proxy обрабатывает auth.aacsearch.com127.0.0.1:8090

Порты

Порт Назначение Доступ
8090 Zitadel Admin/API localhost только
3100 User Info service localhost только
5433 PostgreSQL localhost только
443 HTTPS (через reverse proxy) Публичный

V2 Session API (Panel)

Клиентская панель аутентифицируется через V2 Session API, проксированный API-шлюзом (модуль src/modules/auth/routes.ts). PAT сервисного аккаунта хранится на сервере, клиент никогда его не видит.

Поток

sequenceDiagram
    participant P as Panel SPA
    participant A as API-шлюз
    participant Z as Zitadel V2

    P->>A: POST /api/auth/v2/login
    A->>Z: CreateSession + CheckPassword
    Z-->>A: sessionId, sessionToken
    A-->>P: Cookie: aac_session={sessionId}:{sessionToken}

    Note over P,A: Последующие запросы
    P->>A: Любой запрос (с cookie)
    Note over A: apiAuth middleware:<br/>sessionId из cookie<br/>GET /v2/sessions/{id} (KV кеш 5 мин)<br/>→ tenantId из org_id

Доступные эндпоинты

Эндпоинт Описание
POST /api/auth/v2/login Создание сессии (email + password)
POST /api/auth/v2/register Регистрация + авто-логин
POST /api/auth/v2/password-reset Запрос письма сброса пароля
POST /api/auth/v2/password-set Установка пароля по коду
GET /api/auth/v2/session Проверка текущей сессии
POST /api/auth/v2/logout Удаление сессии

Важные особенности и ограничения

  1. Actions v1 + jwt-bearer: Actions complement_token НЕ выполняются для машинных пользователей с jwt-bearer grant — только для authorization_code flow
  2. Привязка Actions: Используется массив actionIds, а не единственное actionId
  3. Метаданные организации: Значения передаются в base64-кодировке
  4. getMetadata(): Возвращает {count, metadata: [{key, value}]}, а не простой объект
  5. Localhost доступ: Все порты привязаны к 127.0.0.1, публичный доступ только через reverse proxy
  6. Login V2: Использует отдельный контейнер, разделяет сеть с основным Zitadel через service:zitadel
  7. 4-факторная auth в API: Scoped API key → Bearer JWT → V2 Session cookie → Auth.js session (в порядке приоритета)