#Авторизация, ошибки и повторная доставка

В интеграции два секрета, и это главный источник путаницы. Начнём с них.

#Два секрета, а не один

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

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

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

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

Это разные строки, они не взаимозаменяемы и хранятся в разных местах. Если у вас режим pull, вторая пара может вообще не понадобиться: мы к вам не ходим.

#Вы → Smartica

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

В примерах curl этот ключ иногда обозначен переменной SSO_KEY — это то же самое. Название историческое: изначально ключ использовался только для SSO, а сейчас им же авторизуются запросы сценария Import и режима pull.

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

Полный контракт SSO, включая все коды ошибок: Launch URL и SSO.

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

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

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

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

#Smartica → вы

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

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

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

Самая защищённая из доступных — HMAC-подпись: секрет не передаётся по сети вообще, подпись покрывает тело запроса, а старый перехваченный запрос нельзя переиграть. Её описание — в разделе ниже.

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

Честно о разнице: под TLS обе схемы передают статический секрет в заголовке, и криптографически они равноценны. Преимущество Bearer организационное, но от этого не менее реальное:

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

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

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

Одинаковы для обеих схем:

  • не короче 32 случайных символов, сгенерированных программой, а не человеком;
  • разные секреты для тестового и рабочего окружений — адреса контуров в статье Тестовая площадка;
  • отдельная техническая учётная запись без интерактивного входа;
  • права только на согласованные GET и PUT, больше ни на что;
  • плановая ротация с согласованным окном переключения.

Сгенерировать токен:

openssl rand -base64 48

#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. Если он отличается от текущего времени больше чем на 5 минут — отклоните запрос с 401. Это защита от повтора.
  2. Возьмите сырое тело запроса, до разбора JSON. Пересобранный из объекта JSON даст другую строку и другую подпись.
  3. Соберите ту же строку и посчитайте HMAC-SHA256 вашим секретом.
  4. Сравните с присланной подписью за постоянное время. Обычное сравнение строк открывает подбор по таймингу.

Готовые примеры — переключите вкладку на свой стек:

$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: он отсекает перебор до того, как запрос дойдёт до кода.

#Что учесть

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Не включайте их односторонне: адаптер отправит ровно то, что настроено при подключении, и все запросы начнут получать 401.

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

  • Endpoint вашей МИС должен иметь валидный публично доверенный 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 может сохранить этот текст для диагностики и показать его пользователю. Поэтому message должен быть одновременно понятным и безопасным.

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

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

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

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

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

#Timeout

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

Как правильно:

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

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

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

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

Реально повтор происходит так:

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

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

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

Подробнее про слияние полей и стенограммы: PUT: запись протокола.

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

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

Автоматические повторы на 401 и 403 бесполезны и вредны: неудачные попытки авторизации ограничены 30 в минуту, и цикл повторов заблокирует клинику целиком.

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

#Health check

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

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

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

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

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

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

Разбор по конкретным симптомам — в статье Диагностика: если что-то не работает.

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

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