Подписанная личность
Способ дать AI-оператору доступ к данным конкретного пользователя, не передавая нам его токен. Ваш бэкенд подписывает идентификатор пользователя, мы проверяем подпись и вызываем ваше API с подтверждённым идентификатором — токена вашего пользователя у нас не существует ни в каком виде.
Когда нужен этот режим
- Вы не хотите, чтобы токены ваших пользователей покидали ваш периметр.
- Авторизация на сайте построена на httpOnly-куках — тогда
setToken()неприменим в принципе: JavaScript не может прочитать такую куку.
Сравнение с токеном пользователя
| Токен пользователя | Подписанная личность | |
|---|---|---|
| Что хранится у нас | токен пользователя (в зашифрованном виде, до 24 часов) | ничего, кроме подписанного вами утверждения |
| Работает при httpOnly-куках | нет | да |
| Кто проверяет права на данные | ваш существующий auth-слой | ваш код в самой ручке |
| Что нужно сделать на вашей стороне | выпустить токен для чата | подписать идентификатор и проверять нашу подпись |
В этом режиме мы удостоверяем только личность. Проверку «имеет ли этот пользователь право на запрошенные данные» ваш бэкенд должен делать сам — в режиме токена её бесплатно выполнял ваш auth-слой.
Шаг 1. Настройте интеграцию и выпустите секрет
В API-интеграции выберите authMode: "signed_identity", а у ручек с персональными данными включите useUserToken: true. Несмотря на историческое имя поля, в этом режиме оно включает передачу подтверждённой личности, а не токена пользователя. JSON создания и второй вариант в редакторе кода приведены в разделе «API-интеграции AI-агента».
В админ-панели: AI-агенты → нужный агент → API → Способ авторизации → Подписанная личность → Выпустить секрет.
Секрет показывается один раз — сохраните его в конфиге вашего бэкенда. Он используется для двух вещей: вы им подписываете личность, мы им подписываем запросы к вам. После выпуска публичный контракт показывает hasSigningSecret: true, но никогда не возвращает само значение.
Публичного v1 endpoint для выпуска или ротации секрета нет: это кабинетная операция с правами администратора. При выпуске нового секрета старый перестаёт действовать сразу — обновляйте его на своей стороне одновременно.
Шаг 2. Подпишите личность на своём бэкенде
Обычный JWT, алгоритм HS256:
| Claim | Значение |
|---|---|
sub | идентификатор пользователя в вашей системе — он придёт в ваше API |
exp | срок жизни. Держите коротким (например, час): после истечения AI автоматически теряет доступ |
// сервер вашего сайта, при рендере страницы для залогиненного пользователя
const identity = jwt.sign({ sub: user.id }, APX_INTEGRATION_SECRET, {
algorithm: 'HS256',
expiresIn: '1h',
});Секрет остаётся на сервере — в браузер уходит только готовая подпись. Поэтому режим и работает с httpOnly-куками: страница рендерится уже с подписью, читать куку из JS не требуется.
Шаг 3. Передайте подпись виджету
window.apxChat.init('ВАШ_ТОКЕН');
window.apxChat.setIdentity(identityJwt);
// при выходе из аккаунта
window.apxChat.clearIdentity();Жизненный цикл — такой же, как у токена пользователя: значение удаляется при закрытии последней вкладки, по clearIdentity() и автоматически не позже чем через сутки, а при переподключении виджет отправляет его сам.
Шаг 4. Что придёт в ваше API
У ручек, где включён флаг «передавать пользователя», появятся заголовки:
| Заголовок | Описание |
|---|---|
X-Apx-User-Id | sub из вашего JWT — подпись уже проверена нами |
X-Apx-Timestamp | время запроса, Unix-секунды |
X-Apx-Signature | v1=<hex> — HMAC-SHA256 запроса вашим секретом |
Если подпись личности невалидна или истекла, запрос придёт без этих заголовков — решение отказать остаётся за вами.
Шаг 5. Проверьте нашу подпись
Подписывается канонизированная строка, склеенная точками:
<timestamp>.<METHOD>.<path?query>.<userId>.<sha256(body)>METHOD— в верхнем регистре,path?query— путь ровно в том виде, в котором пришёл запрос.sha256(body)— hex-хеш сырого тела. Для запросов без тела — хеш пустой строки.
const crypto = require('crypto');
function verifyApxSignature(req, secret) {
const timestamp = req.get('X-Apx-Timestamp');
const userId = req.get('X-Apx-User-Id');
const provided = req.get('X-Apx-Signature');
if (!timestamp || !userId || !provided) return false;
// Отклонение больше 5 минут — отбрасываем: защита от повторной отправки
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const bodyHash = crypto
.createHash('sha256')
.update(req.rawBody || '')
.digest('hex');
const payload = [timestamp, req.method.toUpperCase(), req.originalUrl, userId, bodyHash].join(
'.'
);
const expected = 'v1=' + crypto.createHmac('sha256', secret).update(payload).digest('hex');
const a = Buffer.from(provided);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Считайте хеш от сырых байтов тела, до JSON.parse — пересериализация меняет строку и ломает
подпись. И сравнивайте подписи функцией с постоянным временем (timingSafeEqual), а не обычным
===.
Что гарантируется
- Токен вашего пользователя нам не передаётся и у нас не хранится.
- Подпись личности проверяется на каждый вызов; истёкший
exp= доступа больше нет, отдельного отзыва не требуется. - Секрет хранится у нас в зашифрованном виде и никогда не отдаётся обратно через API — только признак, что он выпущен.
- Ни оператор, ни AI-модель не видят секрет: заголовки формируются на сервере в момент вызова.
- Запросы уходят только на адреса, прописанные вами в интеграции; переадресации запрещены.