lenec ru

← все посты

Авторизация через Госуслуги (ЕСИА) для Node-приложения

17K

ЕСИА (Единая система идентификации и аутентификации) — это про вход через Госуслуги. Подключение значительно сложнее, чем Яндекс или VK: нужен сертификат и ГОСТ-подпись, отдельная регистрация в техпортале, согласование на каждое окружение. Я проходила этот путь дважды для коммерческих сервисов, у которых был кейс «вход для верифицированных физлиц». Расскажу, как это устроено, и что важно понимать, прежде чем сказать клиенту «да, ЕСИА за неделю подключим».

Сразу важный момент

ЕСИА работает не как обычный OAuth. Это профильный сервис идентификации, и доступ к нему даётся не каждому. Чтобы получить продакшен-окружение:

  • Юрлицо подаёт заявку через тех. портал ЕСИА.
  • Доказывает обоснованность: какие данные пользователей будут запрашиваться, зачем, на каком основании.
  • Получает доступ к тестовому контуру, проходит интеграционное тестирование.
  • Согласует продакшен-доступ.

На моём опыте между «решили подключать» и «работает в проде» проходит 1–2 месяца. Если у тебя ETA в неделю — забудь, не получится.

Тестовый контур (SVCDEV)

Для разработки используется отдельный контур ЕСИА — SVCDEV. Он стоит на адресе https://esia-portal1.test.gosuslugi.ru. У него отдельные учётные записи, отдельные сертификаты и отдельные пользователи-«ученики».

Доступ к тесту получается быстрее, чем к продакшену, но всё равно через заявку. Без неё — никак, в открытом виде SVCDEV не работает.

Как устроен флоу

Базово это OAuth 2.0 с авторизационным кодом, но с парой особенностей:

  1. Запрос /aas/oauth2/v3/ac с client_id, scope, redirect_uri, state, timestamp и подписью.
  2. Пользователь логинится на Госуслугах и подтверждает.
  3. Возврат на redirect_uri с code.
  4. Запрос /aas/oauth2/v3/te на обмен code на access_token. Тоже с подписью.
  5. С access_token идём в /rs/prns/{oid} за персональными данными.

Каждый запрос к ЕСИА сопровождается криптографической подписью клиента. Подпись идёт по ГОСТ Р 34.10–2012 и считается специальной библиотекой. Без подписи запросы отвергаются — это и есть главное отличие от Яндекса.

Сертификат и подпись

Тебе нужен квалифицированный сертификат электронной подписи (КЭП), выпущенный аккредитованным УЦ. Заявка на КЭП — отдельная процедура, обычно через тот же УЦ, который выдавал юрлицу подпись для отчётности.

Закрытый ключ хранится либо на токене (Rutoken, JaCarta), либо в файловом хранилище защищённого носителя. Для серверной интеграции я выбираю файловый ключ + криптопровайдер на сервере.

Чем подписывать на Node

В Node нет встроенной поддержки ГОСТ. Варианты:

  • node-cryptopro — обёртка над утилитой cryptcp/csptest от КриптоПро. Требует установленный КриптоПро CSP на сервере. Самое распространённое решение в продакшене.
  • jsrsasign + node-forge с собственными ГОСТ-расширениями — на бумаге возможно, на практике хрупко и сложно сертифицировать.
  • отдельный микросервис на Java/Python с поддержкой ГОСТ — крутить через REST. Иногда удобно, особенно если Java у вас уже стоит.

Я брала путь с node-cryptopro. Сервер — Astra Linux, на нём установлен КриптоПро CSP, лицензия и сертификаты. Из Node вызываю обёртку, которая подписывает строку и возвращает PKCS#7 base64. Этот PKCS#7 идёт в параметре client_secret запроса к ЕСИА.

sudo apt install cprocsp # после установки КриптоПро CSP
# импортируем закрытый ключ контейнера и сертификат
/opt/cprocsp/sbin/amd64/cpverify ...

Запрос авторизации

Параметры на /aas/oauth2/v3/ac:

function buildAcUrl() {
  const clientId = 'YOUR_MNEMONIC';
  const redirectUri = 'https://app.example.ru/auth/esia/callback';
  const scope = 'openid fullname email mobile';
  const state = crypto.randomBytes(16).toString('hex');
  const timestamp = formatTimestamp(new Date());
  const accessType = 'online';
  const responseType = 'code';

  // Строка для подписи: scope + timestamp + clientId + state
  const stringToSign = `${scope}${timestamp}${clientId}${state}`;
  const clientSecret = signGost(stringToSign); // PKCS#7 detached, base64-url

  const params = new URLSearchParams({
    client_id: clientId,
    client_secret: clientSecret,
    redirect_uri: redirectUri,
    scope,
    response_type: responseType,
    state,
    timestamp,
    access_type: accessType,
  });
  return `https://esia-portal1.test.gosuslugi.ru/aas/oauth2/v3/ac?${params}`;
}

Tip: timestamp в формате YYYY.MM.DD HH:mm:ss +0300. Часовой пояс +0300 — фиксированный, не локальный, иначе подпись считается невалидной.

Обмен кода на токен

На POST /aas/oauth2/v3/te отправляется application/x-www-form-urlencoded:

async function exchangeCode(code: string) {
  const clientId = 'YOUR_MNEMONIC';
  const redirectUri = 'https://app.example.ru/auth/esia/callback';
  const scope = 'openid fullname email mobile';
  const state = crypto.randomBytes(16).toString('hex');
  const timestamp = formatTimestamp(new Date());

  const stringToSign = `${scope}${timestamp}${clientId}${state}`;
  const clientSecret = signGost(stringToSign);

  const body = new URLSearchParams({
    client_id: clientId,
    code,
    grant_type: 'authorization_code',
    redirect_uri: redirectUri,
    timestamp,
    token_type: 'Bearer',
    scope,
    state,
    client_secret: clientSecret,
  });

  const r = await fetch('https://esia-portal1.test.gosuslugi.ru/aas/oauth2/v3/te', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body,
  });
  return r.json() as Promise<{ access_token: string; refresh_token: string; id_token: string; }>;
}

В ответе три токена: access (для запросов в /rs), refresh (для обновления), id_token — JWT с базовыми claims (oid).

Получение профиля

Из id_token декодируем oid — идентификатор пользователя в ЕСИА. Идём за деталями:

async function getProfile(accessToken: string, oid: string) {
  const r = await fetch(`https://esia-portal1.test.gosuslugi.ru/rs/prns/${oid}`, {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  return r.json();
}

async function getContacts(accessToken: string, oid: string) {
  const r = await fetch(`https://esia-portal1.test.gosuslugi.ru/rs/prns/${oid}/ctts?embed=(elements)`, {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  return r.json();
}

async function getDocs(accessToken: string, oid: string) {
  const r = await fetch(`https://esia-portal1.test.gosuslugi.ru/rs/prns/${oid}/docs?embed=(elements)`, {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  return r.json();
}

Endpoint-ов много: /prns/{oid} — анкета, /ctts — контакты, /docs — документы (паспорт, СНИЛС, ИНН), /addrs — адреса, /orgs — организации, /kids — дети. Доступ к каждому управляется scope, который вы запросили.

Что важно про данные

ЕСИА — провайдер чувствительных данных. Хранение и обработка регулируются 152-ФЗ. Без оформления как минимум следующих пунктов — не запускай:

  • Соглашение с пользователем на обработку персданных, в котором перечислены конкретные категории получаемых из ЕСИА данных.
  • Уведомление в Роскомнадзор об обработке.
  • Хранение в БД — минимум, желательно псевдонимизация, для чувствительных полей — отдельные ключи.
  • Журналы доступа к персданным.

Если ты не уверен в деталях — рядом должен быть юрист или DPO, который согласует. С ЕСИА не та история, в которой можно «потом разобраться».

Подводные камни

Несоответствие сертификата

В тех. портале ЕСИА ты загружаешь публичный сертификат, который соответствует контейнеру с закрытым ключом. Если попробуешь подписывать другим — получишь ошибку про несоответствие. Любая смена сертификата (например, окончание срока действия — обычно год) требует обновления через тех. портал, и это занимает время.

Часы на сервере

Из-за timestamp в подписи расхождение времени более чем на 5 минут даёт ошибку. Включай chrony/ntpd обязательно. У меня один раз отвалилось всё после миграции на новый сервер, где время было сдвинуто на 7 минут.

Тест и прод — разные мнемоники

На тестовом контуре client_id один, на проде другой. Помимо мнемоники меняются и URL: тест на esia-portal1.test.gosuslugi.ru, прод — на esia.gosuslugi.ru. У меня в коде это переменные среды.

Кодировка строки для подписи

Для русских scope (если используются кириллические значения) — UTF-8. Для целочисленных параметров timestamp фиксированный формат. Запутаться легко, потому что ошибка про подпись звучит одинаково «invalid signature».

Проверка ЕСИА

Перед подачей на интеграционный тест ЕСИА требует пройти их «программу проверок» — список сценариев, которые ты должен прогнать на тестовом контуре. Часть прогоняется автоматически, часть глазами с приёмочной комиссией. Готовь скриншоты, готовься к разговору с интеграторами на стороне ЕСИА.

Вывод

ЕСИА в Node — это не «подключи библиотеку и забудь». Это:

  • Минимум полтора-два месяца ожидания согласований.
  • Сервер с КриптоПро или аналогом, поддерживающим ГОСТ.
  • Юридический контур по 152-ФЗ.
  • Тестирование на SVCDEV, прохождение программы проверок.
  • Регулярное продление сертификатов и обслуживание.

Если ты делаешь массовый сервис, где «вход через Госуслуги» — реальная необходимость (например, верификация физлиц для финансового продукта или госзакупок), — оно того стоит. Если хочется просто упростить логин — Яндекс ID и VK ID гораздо дешевле и быстрее.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.