#Авторизация и ошибки

#Два секрета

Запросы идут в обе стороны. У каждого направления свой секрет.

Направление Кто хранит Схема Как называется
Вы → smartica.ai Вы Authorization: Bearer <ключ> API-ключ клиники
smartica.ai → вы smartica.ai HMAC-подпись, Bearer-токен или HTTP Basic, всегда поверх HTTPS Учётные данные вашего API

Мы выдаём вам API-ключ клиники. Этим ключом вы подписываете запрос за ссылкой для входа врача. В режиме pull этим ключом вы подписываете запросы за готовыми протоколами.

Вы выдаёте нам учётные данные вашего API. Этими данными мы подписываем запросы GET и PUT к вашей МИС.

API-ключ клиники и учётные данные вашего API — разные строки. Не подменяйте их друг другом. Храните их в разных местах. В режиме pull учётные данные вашего API могут не понадобиться. В этом режиме мы не вызываем вашу МИС.

#Вы → smartica.ai

Authorization: Bearer <API-ключ клиники>

В примерах curl этот ключ иногда обозначен переменной SSO_KEY. Это тот же API-ключ клиники. Изначально ключ использовался только для SSO. Сейчас этим ключом авторизуются запросы сценария Import и режима pull.

Храните ключ только на сервере. Не помещайте ключ в JavaScript, в конфигурацию фронтенда, в мобильное приложение и в параметры страницы.

Полный контракт SSO, включая все коды ошибок, описан в статье Ссылка для входа врача.

#Ротация ключа

При выпуске нового ключа предыдущий продолжает работать 72 часа. Порядок действий:

  1. Запросите новый ключ.
  2. Пропишите новый ключ во всех окружениях, где используется старый.
  3. Проверьте, что запросы проходят с новым ключом.
  4. Дождитесь окончания 72 часов. Старый ключ отключится сам.

Если вы не успеваете за 72 часа, SSO возвращает 401. В режиме pull протоколы не забираются.

#smartica.ai → вы

Здесь вы решаете, как мы авторизуемся при вызове ваших GET и PUT. Доступны три схемы. Каждая схема работает только поверх HTTPS.

#Какую схему выбрать

Схема Что приходит в запросе Когда использовать
HMAC-подпись Smartica-Signature и Smartica-Timestamp Если готовы проверять подпись
Bearer-токен Authorization: Bearer <токен> По умолчанию для новых подключений
HTTP Basic Authorization: Basic <base64(логин:пароль)> Если ваша МИС не может принять токен

HMAC-подпись защищает сильнее остальных схем. Секрет не передаётся по сети. Подпись покрывает тело запроса. Перехваченный запрос нельзя отправить повторно. Описание читайте в разделе «HMAC-подпись запроса».

Если вы не можете проверить подпись, по умолчанию мы настраиваем Bearer.

Под TLS Bearer и HTTP Basic передают статический секрет в заголовке. Криптографически Bearer и HTTP Basic равноценны. Преимущество Bearer организационное:

  • Basic провоцирует завести «пользователя» и переиспользовать существующую учётную запись с широкими правами. Токен создаётся под одну задачу.
  • Логин и пароль часто вставляют в адрес вида https://user:pass@host. Из этого адреса логин и пароль попадают в логи, прокси и заголовок Referer. Токен в такой адрес не вставляют.
  • Токен — одна непрозрачная строка. Токен можно сгенерировать с высокой энтропией. Токен можно заменить целиком. Пароль для Basic нередко придумывает человек.
  • base64 в Basic часто принимают за шифрование. base64 только кодирует данные. base64 читается одной командой.

Если вы выбираете Basic, сделайте пароль длинным и случайным, как токен.

#Требования к секрету

Требования одинаковы для Bearer и HTTP Basic:

  • Секрет не короче 32 случайных символов.
  • Секрет генерирует программа. Не придумывайте секрет вручную.
  • Для тестового и рабочего окружений используйте разные секреты. Адреса контуров читайте в статье Тестовая площадка.
  • Заведите отдельную техническую учётную запись без интерактивного входа.
  • Выдайте права только на согласованные GET и PUT.
  • Запланируйте ротацию с согласованным окном переключения.

Сгенерируйте токен:

openssl rand -base64 48

#HMAC-подпись запроса

Мы не передаём секрет по сети. Мы подписываем запрос этим секретом. Вы проверяете подпись тем же секретом у себя.

Что HMAC-подпись даёт поверх токена:

  • Секрет невозможно перехватить, потому что секрет не уходит в заголовке.
  • Подпись покрывает тело. Содержимое протокола нельзя подменить.
  • Подпись покрывает метод и путь. Подпись нельзя переиспользовать на другом endpoint.
  • Есть метка времени. Перехваченный запрос нельзя повторить позже.

Мы добавляем к каждому запросу два заголовка:

Smartica-Timestamp: 1779805800
Smartica-Signature: v1=8f2a...c31d

Подпись считается так:

подписываемая строка = "{timestamp}.{МЕТОД}.{путь}.{тело}"
подпись              = HMAC-SHA256(секрет, подписываемая строка) в hex

Где:

Часть Что подставляется
timestamp Значение заголовка Smartica-Timestamp — время в секундах Unix
МЕТОД GET или PUT заглавными буквами
путь Путь запроса с ведущим слешем, без хоста: /api/for-smartica/encounters/12345
тело Тело запроса ровно как пришло. Для GET — пустая строка

Разделитель — точка. Для GET строка заканчивается точкой, потому что тело пустое.

#Как проверять на своей стороне

  1. Прочитайте Smartica-Timestamp. Если Smartica-Timestamp отличается от текущего времени больше чем на 5 минут, отклоните запрос с 401. Проверка времени защищает от повтора запроса.
  2. Возьмите сырое тело запроса, до разбора JSON. JSON, собранный заново из объекта, даст другую строку. Подпись тоже будет другой.
  3. Соберите ту же строку. Посчитайте HMAC-SHA256 вашим секретом.
  4. Сравните с присланной подписью за постоянное время. Обычное сравнение строк открывает подбор по таймингу.
Обработчик МИСсохраняет протокол
Проверка подписив МИС, до обработчика
smartica.aiподписывает запрос
Подпись
1
Собирает строку и подписывает еёHMAC-SHA256(секрет, строка) → hex
1779805800.
PUT.
/api/for-smartica/encounters/12345.
{"encounter_id":"12345","fields":[…]}
2
smartica.ai → МИС
Запрос с подписью
PUT /api/for-smartica/encounters/12345
Smartica-Timestamp · Smartica-Signature
Проверкадо разбора JSON и до записи в базу
3
Метка времени отличается от текущей не больше чем на 5 минут
4
Взять сырое тело запроса
Не собирайте JSON заново из объекта: другая строка — другая подпись.
5
Посчитать HMAC-SHA256 тем же секретом
6
Сравнить с присланной подписью за постоянное время
Подпись совпала
7
Проверка → обработчик
Передать запрос дальше
МИС → smartica.ai
200 и JSON: сохранено
Не совпалаили метка времени устарела
МИС → smartica.ai
401
обработчик не вызывается

Примеры для разных стеков:

$timestamp = $_SERVER['HTTP_SMARTICA_TIMESTAMP'] ?? '';
$received = str_replace('v1=', '', $_SERVER['HTTP_SMARTICA_SIGNATURE'] ?? '');

if ($timestamp === '' || abs(time() - (int) $timestamp) > 300) {
    http_response_code(401);
    exit('Просроченная подпись');
}

// Сырое тело, а не $_POST и не json_decode.
$body = file_get_contents('php://input');

$payload = implode('.', [
    $timestamp,
    strtoupper($_SERVER['REQUEST_METHOD']),
    parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH),
    $body,
]);

$expected = hash_hmac('sha256', $payload, getenv('SMARTICA_SIGNING_SECRET'));

if (! hash_equals($expected, $received)) {
    http_response_code(401);
    exit('Неверная подпись');
}
const crypto = require('crypto');

// Важно: нужно сырое тело. В Express — express.raw() или verify-колбэк.
function verifySmarticaSignature(req, res, next) {
  const timestamp = req.get('Smartica-Timestamp') || '';
  const received = (req.get('Smartica-Signature') || '').replace(/^v1=/, '');

  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
  if (!timestamp || age > 300) {
    return res.status(401).json({ message: 'Просроченная подпись' });
  }

  const body = req.rawBody ? req.rawBody.toString('utf8') : '';
  const payload = `${timestamp}.${req.method.toUpperCase()}.${req.path}.${body}`;
  const expected = crypto
    .createHmac('sha256', process.env.SMARTICA_SIGNING_SECRET)
    .update(payload, 'utf8')
    .digest('hex');

  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');

  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(401).json({ message: 'Неверная подпись' });
  }
  next();
}
import hashlib
import hmac
import os
import time


def verify_smartica_signature(method: str, path: str, raw_body: bytes, headers) -> bool:
    timestamp = headers.get("Smartica-Timestamp", "")
    received = headers.get("Smartica-Signature", "").removeprefix("v1=")

    if not timestamp or abs(int(time.time()) - int(timestamp)) > 300:
        return False

    # raw_body — сырые байты запроса, до разбора JSON.
    payload = f"{timestamp}.{method.upper()}.{path}.{raw_body.decode('utf-8')}"
    expected = hmac.new(
        os.environ["SMARTICA_SIGNING_SECRET"].encode(),
        payload.encode(),
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(expected, received)
// Сырое тело: включите EnableBuffering() до чтения.
var timestamp = Request.Headers["Smartica-Timestamp"].ToString();
var received = Request.Headers["Smartica-Signature"].ToString().Replace("v1=", "");

if (string.IsNullOrEmpty(timestamp) ||
    Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - long.Parse(timestamp)) > 300)
{
    return Results.Unauthorized();
}

Request.EnableBuffering();
using var reader = new StreamReader(Request.Body, leaveOpen: true);
var body = await reader.ReadToEndAsync();
Request.Body.Position = 0;

var payload = $"{timestamp}.{Request.Method.ToUpperInvariant()}.{Request.Path}.{body}";
var secret = Environment.GetEnvironmentVariable("SMARTICA_SIGNING_SECRET")!;

using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var expected = Convert
    .ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(payload)))
    .ToLowerInvariant();

if (!CryptographicOperations.FixedTimeEquals(
        Encoding.UTF8.GetBytes(expected),
        Encoding.UTF8.GetBytes(received)))
{
    return Results.Unauthorized();
}
// Сырое тело: оберните запрос в ContentCachingRequestWrapper.
String timestamp = request.getHeader("Smartica-Timestamp");
String received = request.getHeader("Smartica-Signature").replace("v1=", "");

if (timestamp == null
        || Math.abs(Instant.now().getEpochSecond() - Long.parseLong(timestamp)) > 300) {
    throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Просроченная подпись");
}

String body = new String(request.getContentAsByteArray(), StandardCharsets.UTF_8);
String payload = timestamp + "." + request.getMethod().toUpperCase()
        + "." + request.getRequestURI() + "." + body;

Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));

// HexFormat доступен с Java 17.
String expected = HexFormat.of()
        .formatHex(mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)));

if (!MessageDigest.isEqual(
        expected.getBytes(StandardCharsets.UTF_8),
        received.getBytes(StandardCharsets.UTF_8))) {
    throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Неверная подпись");
}

#Если ваша МИС на 1С

В платформе 1С:Предприятие нет встроенной функции HMAC. Объект ХешированиеДанных считает только простые хеши. Объект не принимает секретный ключ. HMAC можно реализовать через функцию подписи из БСП или вручную через XOR и вложенное хеширование. Такая реализация сложнее, чем в остальных стеках. Ошибку в такой реализации можно не заметить.

Для интеграций на 1С мы рекомендуем Bearer-токен. Проверка сводится к сравнению строки. Нужный уровень защиты дают IP allowlist и требования к секрету.

Функция SmarticaEncounterPUT(Запрос)

    ОжидаемыйЗаголовок = "Bearer " + Константы.SmarticaВходящийТокен.Получить();
    ПолученныйЗаголовок = "";

    Для Каждого Заголовок Из Запрос.Заголовки Цикл
        Если НРег(Заголовок.Ключ) = "authorization" Тогда
            ПолученныйЗаголовок = Заголовок.Значение;
        КонецЕсли;
    КонецЦикла;

    Если ПолученныйЗаголовок <> ОжидаемыйЗаголовок Тогда
        Ответ = Новый HTTPСервисОтвет(401);
        Ответ.УстановитьТелоИзСтроки("{""message"": ""Неверный токен""}");
        Возврат Ответ;
    КонецЕсли;

    ТелоЗапроса = Запрос.ПолучитьТелоКакСтроку();
    // Разбор JSON и сохранение протокола по encounter_id.

    Ответ = Новый HTTPСервисОтвет(200);
    Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
    Ответ.УстановитьТелоИзСтроки("{""encounter_id"": ""12345"", ""status"": ""saved""}");

    Возврат Ответ;

КонецФункции

В 1С нет встроенного сравнения за постоянное время. Для таких подключений важен IP allowlist. IP allowlist отсекает перебор до того, как запрос дойдёт до кода.

#Что учесть

  • Мы генерируем секрет при подключении. Мы передаём секрет по защищённому каналу.
  • При HMAC заголовок Authorization не отправляется. Подпись служит авторизацией. Если ваш прокси требует Authorization, скажите об этом. Мы подберём комбинацию.
  • Берите путь, который видит ваше приложение. Если прокси срезает префикс, подпись не сойдётся. Сверьте путь на тестовом прогоне.
  • Проверяйте подпись до разбора тела и до любой бизнес-логики.

#Усиление защиты

Схема авторизации — только один слой. Без TLS 1.2 и публично доверенного сертификата схема не защищает канал. Меры ниже не требуют изменений в контракте. Эти меры дают больше, чем выбор между Bearer и Basic:

Мера Что даёт
TLS 1.2 и выше с публично доверенным сертификатом Защищает канал
IP allowlist на наши адреса Запрос с чужого адреса не дойдёт до приложения
Отдельный технический аккаунт с минимальными правами Ограничивает ущерб при утечке секрета
Только методы GET и PUT на этом адресе Убирает лишнюю поверхность атаки
Rate limit на стороне МИС Защита от перебора секрета
Логирование неудачных попыток авторизации Раннее обнаружение подбора
Регулярная ротация Ограничивает срок жизни утёкшего секрета

Актуальные адреса для allowlist запросите при подключении.

#Ротация секрета

У секрета для входящих запросов нет окна грации. Адаптер в каждый момент отправляет ровно один действующий секрет. Если вы смените секрет у себя раньше, чем у нас, запросы получают 401.

Безопасный порядок:

  1. Сгенерируйте новый секрет. Пока не отключайте старый секрет.
  2. Настройте свой endpoint так, чтобы он принимал оба секрета. Сначала проверяйте новым секретом. При неудаче проверяйте старым секретом. Для Bearer и Basic сравните строку с двумя значениями. Для HMAC проверьте подпись двумя секретами.
  3. Передайте новый секрет нам по защищённому каналу. Согласуйте время переключения.
  4. Мы меняем настройку у себя.
  5. Убедитесь по своим логам, что запросы приходят с новым секретом.
  6. Уберите поддержку старого секрета.

Второй пункт — единственный способ обойтись без окна ошибок. Для второго пункта нужно несколько строк кода.

Не путайте эту ротацию с ротацией API-ключа клиники. У API-ключа клиники окно грации есть. Старый API-ключ клиники действует ещё 72 часа.

#Схемы, которые требуют доработки

Эти схемы в текущем адаптере не реализованы. Если одна из схем обязательна по вашим требованиям безопасности, скажите об этом до начала разработки. Нам нужно оценить доработку.

Схема Что даёт дополнительно
mTLS — взаимный TLS Стороны проверяют сертификаты друг друга. Одного перехваченного заголовка для запроса недостаточно
OAuth 2.0 client credentials Короткоживущие токены вместо постоянного секрета

Не включайте эти схемы только у себя. Адаптер отправит ровно то, что настроено при подключении. Все запросы получат 401.

#Сетевой доступ

  • У endpoint вашей МИС должен быть валидный публично доверенный TLS-сертификат. Самоподписанный сертификат не подойдёт.
  • Не просите отключить проверку TLS. Мы не отключим проверку TLS.
  • Если у вас IP allowlist, запросите у нас актуальные адреса при подключении.
  • Не составляйте allowlist по догадкам. Неверный список даёт timeout или 403. При отладке причина неочевидна.
  • Согласуйте VPN, прокси и DNS до сквозного теста.

#Каким должен быть успешный ответ вашей МИС

Для GET и PUT возвращайте:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

Тело — валидный JSON-объект.

204 No Content, пустое тело и HTML считаются ошибкой интеграции, даже если код формально относится к 2xx. Частая причина — прокси или балансировщик, который подменяет ответ приложения своей страницей. Проверяйте ответ с внешней машины.

#Формат ошибки вашей МИС

Рекомендуемый ответ:

{
  "message": "Приём не найден"
}

smartica.ai может сохранить message для диагностики. smartica.ai может показать message пользователю. Поэтому message должен быть понятным и безопасным.

Никогда не включайте в message:

  • стектрейсы.
  • SQL-запросы и внутренние пути.
  • логины, пароли, токены.
  • персональные данные пациента.
  • фрагменты конфигурации сервера.

#Коды ответов вашего API

Код Значение Что произойдёт
200 Успех, тело содержит JSON smartica.ai продолжает обработку
400 Некорректный запрос Нужно исправить контракт
401 Неверные учётные данные Проверить или ротировать секрет
403 Доступ запрещён Проверить права, VPN и allowlist
404 Приём не найден Проверить ID и окружение
409 Конфликт состояния Решается на вашей стороне
422 Ошибка в полях Исправить конкретные поля
429 Ваш лимит запросов Согласовать лимиты и Retry-After
5xx Временная ошибка Врач увидит ошибку и сможет повторить

Любой ответ не из 2xx, сетевой сбой, timeout или невалидный JSON означают неуспех текущей операции.

#Timeout

HTTP timeout smartica.ai для запросов к вашей МИС — 30 секунд. Это верхний предел ожидания. После 30 секунд smartica.ai прекращает ждать. Не считайте 30 секунд целевым временем ответа.

Как отвечать:

  • GET читает готовые данные приёма.
  • PUT сохраняет присланное.
  • Тяжёлую внутреннюю обработку запускайте после надёжного сохранения.
  • Не отвечайте 200, пока данные не приняты устойчиво.

200 для smartica.ai означает «сохранено». Если вы ответите успехом, а потом потеряете данные, повтора может не быть.

#Повторная доставка

Контракт не гарантирует автоматический повтор каждого неуспешного GET или PUT.

Повтор происходит так:

  • После ошибки GET врач заново открывает приём.
  • После ошибки PUT врач нажимает повтор отправки в интерфейсе smartica.ai.
  • Новая запись или перезаполнение в том же приёме порождает новый PUT.

Отсюда два обязательных требования:

  1. PUT должен быть идемпотентным по encounter_id.
  2. МИС не должна рассчитывать ни на доставку ровно один раз, ни на фиксированное число попыток.

Подробнее про слияние полей и стенограммы читайте в статье Запись протокола в карту.

#Как обрабатывать ошибки SSO в интерфейсе МИС

Код Что показать врачу и что сделать
401 Сообщить об ошибке конфигурации. Не повторять
403 Показать message, предложить обратиться в поддержку smartica.ai
404 Проверить выданный адрес, затем обратиться в поддержку
422 Показать ошибку данных из объекта errors
429 Заблокировать повтор до времени из Retry-After
5xx или сетевой сбой Показать временную ошибку и кнопку повторного запуска

Не повторяйте 401 и 403 автоматически. Неудачные попытки авторизации ограничены 30 в минуту. Цикл повторов заблокирует клинику целиком.

Только при сетевой или временной серверной ошибке предложите врачу обычный вход в smartica.ai по логину и паролю как временный обходной путь. Обычный вход не исправит неверный ключ, конфликт организаций или невалидный encounter_id.

#Health check

Можно поднять отдельный endpoint для ручной проверки:

GET /api/for-smartica/health
{
  "status": "ok"
}

Этот endpoint не входит в обязательный протокол. smartica.ai не вызывает его автоматически. Если вы хотите использовать endpoint для проверки связности при разборе инцидентов, сообщите адрес нашей команде.

#Порядок диагностики

Если что-то не работает, проверяйте сверху вниз:

  1. Проверьте окружение и хост. Адреса смотрите в статье Тестовая площадка.
  2. Проверьте, что encounter_id совпадает в SSO, в адресе и в теле.
  3. Проверьте, что endpoint доступен из внешней сети.
  4. Проверьте, что TLS-сертификат валиден.
  5. Проверьте, что ответ содержит Content-Type: application/json.
  6. Проверьте, что вместо JSON не приходит 204, HTML или страница прокси.
  7. Проверьте, что повторный PUT идемпотентен.

Разбор по конкретным симптомам читайте в статье Если запросы не проходят.

#Связанные статьи

← Все статьи: Интеграция с МИС Поиск по базе знаний