Дія.Підпис для бізнесу: технічні виклики інтеграції з державним сервісом
ECDSA + SHA256 для хешування. Redis key mismatch між start та verify. QR-код і deep link. Оновлення даних ФОП/ТОВ при кожному логіні. 4 фікси за добу. Реальна історія інтеграції з Дією — без прикрас.
Дія.Підпис для бізнесу: технічні виклики інтеграції з державним сервісом
Реальна історія: як ми інтегрували Дію.Підпис і що пішло не так (і як ми це полагодили).
Навіщо Дія.Підпис
Google OAuth — зручний, але не юридично значущий. Для LegalTech-платформи це проблема: ми маємо знати, що користувач — це конкретна фізична особа або ФОП/ТОВ, а не просто власник Gmail-акаунту.
Дія.Підпис (Diia.Sign) вирішує це:
- Верифікована ідентичність — прив'язка до ІПН/ЄДРПОУ
- Юридична сила — кваліфікований електронний підпис
- Зручність — QR-код або deep link, без токенів чи USB-ключів
Підключення до Дії — обов'язковий крок для будь-якої платформи, яка працює з юридичними документами в Україні.
Архітектура: як це працює
Флоу автентифікації
Користувач → LEX (натискає "Увійти через Дію")
↓
LEX Backend → Diia API: POST /api/v2/auth/acquirer/branch/offer/request
↓
Diia API → LEX: { deeplink, requestId }
↓
LEX Frontend → показує QR-код (з deeplink) або редіректить на deep link
↓
Користувач → сканує QR у Дії, підтверджує
↓
Diia API → LEX Callback: POST /api/diia/callback
↓
LEX Backend → розшифровує дані, створює/оновлює користувача
↓
LEX Backend → видає JWT, редіректить на фронтенд
Ключові компоненти
| Компонент | Технологія |
|---|---|
| Хешування requestId | ECDSA + SHA256, Base64 |
| Зберігання стану | Redis (TTL 5 хвилин) |
| Розшифрування даних | AES-256-CBC (ключ від Дії) |
| Сесія | JWT з даними користувача |
Проблема 1: ECDSA хешування requestId
Що було
Дія вимагає, щоб requestId був підписаний ECDSA з SHA-256 і закодований у Base64. Документація мінімальна. Наша перша реалізація:
// ❌ Не працювало
const hash = crypto.createHash('sha256')
.update(requestId)
.digest('hex');
Що пішло не так
Дія очікувала не просто хеш, а підпис ECDSA з приватним ключем acquirerToken. Формат підпису — DER, закодований у Base64. Hex-хеш — це зовсім інше.
Як полагодили
// ✅ Правильно
const sign = crypto.createSign('SHA256');
sign.update(requestId);
const signature = sign.sign(privateKey, 'base64');
Ключовий момент: приватний ключ — це не acquirerToken напряму, а PEM-ключ, який генерується при реєстрації в порталі Дії.
Проблема 2: Redis key mismatch
Що було
Флоу: /start створює запит і зберігає requestId в Redis. Коли callback приходить від Дії — бекенд шукає цей requestId в Redis, щоб зіставити з сесією.
// /start endpoint
await redis.set(`diia:request:${requestId}`, sessionData, 'EX', 300);
// /callback endpoint
const session = await redis.get(`diia:auth:${requestId}`);
// 💥 null — ключі не збігаються!
Що пішло не так
Два різних префікси: diia:request: при створенні, diia:auth: при верифікації. Класичний copy-paste баг. Callback приходив, але Redis повертав null, і автентифікація мовчки фейлилась.
Як полагодили
Уніфікували префікс:
const REDIS_PREFIX = 'diia:auth:';
// /start
await redis.set(`${REDIS_PREFIX}${requestId}`, sessionData, 'EX', 300);
// /callback
const session = await redis.get(`${REDIS_PREFIX}${requestId}`);
Проблема 3: Оновлення даних бізнесу
Що було
При першому логіні через Дію ми створювали запис ФОП/ТОВ:
- Назва організації
- ЄДРПОУ/ІПН
- Адреса
- Назви офертів та бранчів
Але ці дані можуть змінюватись: компанія переїхала, змінила назву, оновила контактний email.
Що пішло не так
Другий і подальші логіни ігнорували нові дані від Дії — ми просто знаходили існуючий запис по ЄДРПОУ і пропускали оновлення. Результат: застарілі адреси, старі назви офертів.
Як полагодили — 4 PR за добу
PR #1117 — оновлення назв бранчів та офертів:
// Раніше: створювали тільки якщо не існує
// Тепер: UPDATE при кожному логіні
await db.query(`
UPDATE diia_branches
SET name = $2, address = $3, updated_at = NOW()
WHERE acquirer_id = $1
`, [acquirerId, branchName, address]);
PR #1118 — оновлення існуючих бранчів при ініціалізації:
Додали ON CONFLICT DO UPDATE для ідемпотентності.
PR #1119 — оновлення назв офертів: Назви офертів теж могли змінюватись (наприклад, "Авторизація" → "Вхід через Дію").
PR #1120 — оновлення назви компанії та email:
await db.query(`
UPDATE organizations
SET name = $2, email = $3, updated_at = NOW()
WHERE edrpou = $1
`, [edrpou, companyName, contactEmail]);
Проблема 4: Nginx proto override
Що було
Дія відправляє callback на наш URL. У продакшні — за Cloudflare та Nginx. Nginx проксює запит на бекенд, але губить оригінальний протокол.
Що пішло не так
Бекенд генерував redirect URL з http:// замість https://:
# Дія callback → Nginx → Backend
# Backend бачив: req.protocol = 'http'
# Генерував: http://legal.org.ua/auth/callback
# Браузер: mixed content error
Як полагодили
Nginx конфіг:
proxy_set_header X-Forwarded-Proto $scheme;
Express middleware:
app.set('trust proxy', 1);
// Тепер req.protocol читає X-Forwarded-Proto
Поточний стан інтеграції
| Параметр | Значення |
|---|---|
| Тип підпису | Дія.Підпис (КЕП) |
| Хешування | ECDSA + SHA-256 + Base64 |
| Стан сесії | Redis, TTL 5 хв |
| Шифрування | AES-256-CBC |
| JWT | RS256, 7 днів |
| Оновлення даних | При кожному логіні |
Що отримуємо від Дії
При успішній автентифікації Дія повертає:
- ПІБ
- ІПН
- Дата народження
- Для ФОП/ТОВ: ЄДРПОУ, назва, адреса
Ці дані автоматично синхронізуються з нашою базою при кожному логіні.
Уроки
Документація Дії — мінімальна. Готуйтесь до reverse engineering. Тестове середовище працює інакше, ніж продакшн.
Redis-ключі мають бути константами. Один prefix, один файл з константами. Ніякого дублювання рядків.
Дані потрібно оновлювати при кожному логіні. Не тільки створювати, а й синхронізувати. Бізнес-дані змінюються.
Тестуйте весь флоу end-to-end. Unit-тести не покривають: "callback приходить, але Redis ключ інший". Тільки повний прогін від /start до JWT видає баг.
Nginx — невидимий убивця. X-Forwarded-Proto, X-Real-IP, trust proxy — конфігуруйте до того, як інтеграція піде в продакшн.
Дія.Підпис — це правильний вибір для юридичної платформи. Але шлях від "документація виглядає просто" до "все працює в проді" — це 4 PR за добу і купа нетривіальних багів.
Реєстрація: legal.org.ua