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