Как реализовать интеграцию с ЕСИА на Java без лишних проблем

Что делать и как?

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

Для начала важно было наметить план действий. Наш план включал следующие основные шаги:

  1. зарегистрироваться на технологическом портале ЕСИА;
  2. подать заявки на использование программных интерфейсов ЕСИА в тестовой и промышленной среде;
  3. самостоятельно разработать механизм взаимодействия с ЕСИА (в соответствии с действующим документом «Методические рекомендации по использованию ЕСИА»);
  4. протестировать работу механизма в тестовой и промышленной среде ЕСИА.


Обычно мы разрабатываем наши проекты на Java. Поэтому для программной реализации выбрали:

Введение

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

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

  1. Регистрация ИС в регистре информационных систем ЕСИА
  2. Регистрация ИС в тестовой среде
  3. Выполнение доработки системы для взаимодействия с ЕСИА

В данной статье будет описан только 3 пункт, предыдущие 2 – бюрократия, оставим ее за рамками Хабра. В методичке предлагают реализовать интеграцию 2 способами: SAML или OpenID Connect. Говорят,

Похожее:  Передать показания счетчиков электроэнергии - Триц РФ Тюмень

с 01.01.2022 г. взаимодействие по протоколу SAML 2.0 больше не будет разрешено (только для действующих систем). Для подключения к ЕСИА необходимо будет использовать протокол OAuth 2.0 / OpenID Connect (сейчас доступны оба варианта).

Поэтому мы выбрали красную таблетку второй вариант. Согласно методичке, базовый сценарий аутентификации представляет собой примерно следующие шаги:

Что теперь делать со всеми этими данными?

Мы можем сделать парсинг данных и получать объекты с требуемыми полями. Здесь каждый разработчик может оформлять классы как ему необходимо, в соответствии с техническим заданием.

Пример получения объекта с необходимыми полями:

final ObjectMapper objectMapper = new ObjectMapper()
	.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

String personDataEntityString = esiaPersonDataFetcher
	.apply(ESIA_REST_API_URL   "/prns/"   esiaAccountId);

EsiaPersonDto esiaPersonDto = objectMapper
	.readValue(personDataEntityString, EsiaPersonDto.class);


Заполняем объект esiaPersonDto необходимыми данными, например, контактами:

for (String contactUrl : esiaListDto.getElementUrls()) {
  String contactEntityString = esiaPersonDataFetcher.apply(contactUrl);
  EsiaContactDto esiaContactDto = objectMapper.readValue(contactEntityString, EsiaContactDto.class); // Десериализация контакта
  if (esiaContactDto.getType() == null) continue;
  switch (esiaContactDto.getType().toUpperCase()) {
    case EsiaContactDto.MBT: // Если это номер мобильного телефона, то заполним поле mobilePhone
      esiaPersonDto.setMobilePhone(esiaContactDto.getValue());
      break;
    case EsiaContactDto.EML: // Если это адрес электронной почты, то заполним поле email
      esiaPersonDto.setEmail(esiaContactDto.getValue());
  }
}

Класс EsiaPersonDto выглядит следующим образом:

@Data
@FieldNameConstants(prefix = "")
public class EsiaPersonDto {

  private String firstName;
  private String lastName;
  private String middleName;
  private String birthDate;
  private String birthPlace;
  private Boolean trusted;  // тип учетной записи - подтверждена (“true”) / не подтверждена (“false”)
  private String status;    // статус УЗ - Registered (зарегистрирована) /Deleted (удалена)
  // Назначение полей непонятно, но они есть при запросе /prns/{oid}
  private List<String> stateFacts;
  private String citizenship;
  private Long updatedOn;
  private Boolean verifying;
  @JsonProperty("rIdDoc")
  private Integer documentId;
  private Boolean containsUpCfmCode;
  @JsonProperty("eTag")
  private String tag;
  // ----------------------------------------
  private String mobilePhone;
  private String email;

  @javax.validation.constraints.Pattern(regexp = "(\d{2})\s(\d{2})")
  private String docSerial;

  @javax.validation.constraints.Pattern(regexp = "(\d{6})")
  private String docNumber;

  private String docIssueDate;

  @javax.validation.constraints.Pattern(regexp = "([0-9]{3})\-([0-9]{3})")
  private String docDepartmentCode;

  private String docDepartment;

  @javax.validation.constraints.Pattern(regexp = "\d{14}")
  @JsonProperty("snils")
  private String pensionFundCertificateNumber;

  @javax.validation.constraints.Pattern(regexp = "\d{12}")
  @JsonProperty("inn")
  private String taxPayerNumber;

  @JsonIgnore
  @javax.validation.constraints.Pattern(regexp = "\d{2}")
  private String taxPayerCertificateSeries;

  @JsonIgnore
  @javax.validation.constraints.Pattern(regexp = "\d{10}")
  private String taxPayerCertificateNumber;
}

Работа по усовершенствованию сервиса будет продолжаться, ведь ЕСИА не стоит на месте.

Documentation

Здесь представлена документация по Esia.NET для .NET Core 3.1 и выше. Nuget-пакеты версии 1.0 и выше.

Для классического .NET Framework документация и реализация находится в ветке dot-net-classic. Также используйте Nuget-пакеты версии 0.1.

Examples

Пример интеграции Esia.NET и ASP.NET Core для реализации авторизации через ЕСИА и получения данных.
Вставьте свои данные для работы примера. В файле EsiaNET.MvcStartup.cs – данные клиент-системы ЕСИА. В файле SignerAppControllersSignController.cs – серийный номер вашего ГОСТ сертификата.

Github – yetihead/esia: модуль идентификации и аутентификации пользователей через есиа для node.js

Модуль идентификации и авторизации пользователей через ЕСИА для Node.js

Перед использованием ознакомтесь с документацией.

  1. Установите
  1. Создайте экземпляр подключения к ЕСИА
  1. Направьте пользвателя в ЕСИА для получения подтверждения
  1. После того как пользователь авторизуется и даст разрешение на доступ к своим данным, он будет перенаправлен по адресу, указанному в redirectUri. Вы получите параметр code, который нужно будет использовать для запроса данных

Метод getAccess возвращает Promise, в который приходит объект результата. Этот объект содержит два поля:

  • marker – маркер доступа. Объект, содержащий поля:
    • response – объект, ответ от ЕСИА при запросе маркера
    • decodedAccessToken – объект, jwt декодированное значение поля access_token, содержащееся в response.
  • data – массив записей данных о пользователе.

В метод getAccess вторым параметром можно передать массив путей для получения записей данных о пользователе. Они будут содержаться в ответе, в поле data, описанном выше. Если параметр не передавать, по умолчанию будет использоваться [‘/’]. Если передать null, то данные о пользователе запрашиваться не будут.

Подробнее о получении информации о пользователе читайте в официальной документации ЕСИА.

Авторизация через госуслуги

Везде идет речь о сертификате (1 файл), о закрытом ключе (1 файл) и пароле к закрытому ключу (строка)

Для обмена с ЕСИА потребуется шифрование, поэтому у вас должен быть свой RSA-ключ для подписи. Закрытый ключ находится у вас, публичный сертификат выгружается в кабинет ЕСИА.

Некие сертификаты, общедоступные по ссылке http://esia.gosuslugi.ru/public/esia.zip

Поскольку ЕСИА тоже будет присылать вам шифрованную информацию, то вам потребуется публичный сертификат ЕСИА, чтобы расшифровать эту информацию.

Файлы от заказчика содержат один файл с расширением cer

Скорей всего – это публичный сертификат. Можете попробовать его открыть. В windows должно открыться стандартное окно с информацией о сертификате.

и папку с несколькими файлами с расширением key (header.key, masks.key, primary.key и тд)

А вот тут сказать сложно. Похоже, что клиент сгенерировал ключ на флешке средствами крипто-про или наподобие, а потом просто скопировал каталог. Вот тут он не прав. Надо выяснять конкретные требования заказчика по интеграции

Для авторизации через госуслуги можно использовать, например, самоподписанные сертификаты:

Сначала создать закрытый ключ и запрос на сертификат

openssl req -newkey rsa:2048 -nodes -keyout domain.key -out domain.csr -sha256

в процессе генерации будет запрос на воод пароля к закрытому ключу.

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

openssl x509 -signkey domain.key -in domain.csr -req -days 1825 -out domain.crt -sha256

у вас будет два файла domain.key (закрытый ключ) и domain.crt (сертификат). Сертификат потом надо будет выгрузить в кабинет ЕСИА.

Как мне эти данные использовать?

Очень помог в интеграции с ЕСИА пакет https://vhod-v-lichnyj-kabinet.ru/ekapusta/oauth2-esia
Описание работы с ним https://vhod-v-lichnyj-kabinet.ru/ru/post/358834/

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

Базовая информация

Мы реализовали сервис интеграции с ЕСИА под Windows, используя КриптоПро CSP. В теории скорее всего можно это все аккуратно упаковать в docker и положить в Linux-образные системы, оставим это на откуп читателю. Для нас же актуальным стеком был следующий:

  • .Net Framework 4.8
  • КриптоПро CSP, КриптоПро .Net
  • Сертификат с закрытым ключом, полученный при регистрации ИС в реестре ИС ЕСИА (на пункте 1 из Введения)

Каждый запрос в ЕСИА по соображениям безопасности дополняется полем client_secret, которое формируется как открепленная подпись 4 полей запроса в формате UTF-8:

  • Scope (Скоуп запроса, перечень данных, которые нужно получить из ЕСИА). Например, «fullname gender email mobile usr_org»
  • Timestamp (Текущие дата и время в формате «yyyy.MM.dd HH:mm:ss 0000»)
  • ClientId (Идентификатор ИС, который выдается при регистрации системы в ЕСИА)
  • State (Идентификатор текущего запроса, каждый раз генерируется как Guid.NewGuid().ToString(«D»))
private string GetClientSecret(
	X509Certificate2 certificate, 
	string scope, 
	string timestamp, 
	string clientId, 
	string state)
{
	var signMessage = Encoding.UTF8.GetBytes($"{scope}{timestamp}{clientId}{state}");

	byte[] encodedSignature = SignatureProvider.Sign(signMessage, certificate);
 
	return Base64UrlEncoder.Encode(encodedSignature);
}

Тут SignatureProvider – класс для реализации работы с сертификатами, он довольно просто реализуется. Для подписи использовался алгоритм ГОСТ – импортозамещение и все такое.

Выполнение методов api

Необходимо в контроллере (или где-нибудь ещё) запросить интерфейс IEsiaRestService.
В нём есть метод CallAsync, который и отвечает за актуализацию токенов и общение с API ЕСИА.

Пример запроса:

Данный кусок кода получает oId пользователя, запрашивает все контакты и складывает их JSON-представление в ViewBag.

Гост криптография

В связи с тем что реализовать использование ГОСТ криптографии в .NET Core крайне проблематично, в примерах провайдер реализован в виде отдельного .NET Framework веб-приложения.
Вы можете использовать свою реализацию. Смотрите пример.
DefaultSignProvider в текущем виде работает только в Windows и классическом .NET Framework.

Также необходимо иметь специализированное сертифицированное ПО для работы с ГОСТ криптографией.

Есть замечания / хочу внести вклад

Создавайте issue, предлагайте свои pull request-ы.

Вместе мы сможем сделать отличную библиотеку. 🙂

Зачем нам нужна интеграция с есиа?

В связи с пандемией коронавируса количество офлайн сделок во многих направлениях кредитования начало сокращаться. Клиенты стали «уходить в онлайн», и для нас было жизненно важно укрепить своё онлайн-присутствие на рынке автокредитования. В процессе доработки сервиса «Автокредит» (на Хабре уже есть статья про его разработку) мы решили сделать интерфейс заведения кредитных заявок на сайте банка максимально удобным и простым.

Использование


Для того, чтобы подключиться к ЕСИА с помощью библиотеки вам нужно иметь на руках:

  1. Сертификат, выданный, либо самоподписанный в формате описанном в методических рекомендациях, загруженный на тестовый и боевой сервер ЕСИА.
  2. Выданную службой поддержки ЕСИА учетную запись компании на боевом и тестовом серверах. Она, затем, должна быть указана при создании объекта EsiaSettings вместо строки “YOUR_SYSTEM_ID”.
  3. Учетные записи пользователей на тестовом и боевом серверах ЕСИА для отладки и тестирования.
  4. Публичные ключи ЕСИА (тестовый и боевой) для верификации полученного токена. В открытом доступе этих ключей нет, техподдержка высылает их электронной почтой по требованию.

Чтобы запустить тестовый пример (минимальное веб-приложение на Flask доступно в репозитории библиотеки) нужнопредварительно загруженный на сервер ЕСИА сертификат разместить в файле “esia-connector/examples/res/test.crt”. В этом же каталоге следует разместить ваш приватный ключ под именем “test.key”, а упомянутый выше публичный ключ разместить под именем “esia_pub.key”.Затем запустить приложение Flask, из каталога examples выполнить:

python flask_app.py

Как запустить пример

Для ОС Windows 10 необходимо установить Windows Subsystem for Linux и Ubuntu 18.04 в нём.
Действия выполняются внутри терминала этой ОС.

Данный раздел показывает, как можно запустить пример работы с ЕСИА на Ubuntu 18.04 (или Windows 10 c WSL).
Такая конфигурация выбрана из-за того, что на Linux намного удобнее включается поддержка ГОСТ для openssl.

Сперва необходимо обновить списки пакетов: $ sudo apt update.

Затем устанавливается пакет для поддержки ГОСТ в openssl: $ sudo apt install libengine-gost-openssl1.1.

После этого необходимо открыть файл с настройками openssl: $ sudo nano /etc/ssl/openssl.cnf.

Дописать в начало файла (например, после oid_section = new_oids): openssl_conf = openssl_def.

Дописать в конец файла:

Для проверки установки движка gost можно выполнить следующую команду и сравнить результат с представленным ниже:

$ openssl engine gost -c
(gost) Reference implementation of GOST engine
 [gost89, gost89-cnt, gost89-cnt-12, gost89-cbc, grasshopper-ecb, grasshopper-cbc, grasshopper-cfb, grasshopper-ofb, grasshopper-ctr, md_gost94, gost-mac, md_gost12_256, md_gost12_512, gost-mac-12, gost2001, gost-mac, gost2022_256, gost2022_512, gost-mac-12]

Теперь необходимо сгенерировать ключи для ЕСИА при помощи команд:

$ openssl req -x509 -newkey gost2022_256 -pkeyopt paramset:A -nodes -keyout esia.key -out esia.pem -days 3650 -engine gost
$ openssl pkcs12 -export -out esia.pfx -inkey esia.key -in esia.pem -engine gost

Данные о стране, городе, имени сертификата можно вбивать любые, они не играют роли для ЕСИА.

Чтобы проверить, что подпись данных в openssl работает, можете использовать следующую команду:

Должен вернуться вывод с огромным base64-текстом, разбитым на несколько строк.

Для регистрации ключа в ЕСИА на технологический портал требуется загружать файл .pem.

Теперь для запуска примера потребуется:

Запуск примера можно проделать следующим образом:

$ dotnet build
$ dotnet run -p samples/EsiaSample/

Подключение

  1. Добавьте NuGet-пакет AISGorod.AspNetCore.Authentication.Esia.
  2. Добавьте в Startup.cs следующие строки (ниже данные для примера):

Получение url для переадресации


Первый шаг ― это получение авторизационного кода. В нашем случае это делает отдельный сервис с переадресацией на страницу авторизации портала Госуслуг (расскажем об этом немного подробнее).

Сначала мы инициализируем переменные ESIA_AUTH_URL (адрес ЕСИА) и API_URL (адрес, на который происходит редирект в случае успешной авторизации). После этого создаем объект EsiaRequestParams, который содержит в своих полях параметры запроса к ЕСИА, и сформируем ссылку esiaAuthUri.

public Response loginByEsia() throws Exception {
  final String ESIA_AUTH_URL = dao.getEsiaAuthUrl(); // Адрес ЕСИА
  final String API_URL = dao.getApiUrl(); // Адрес, на который произойдет редирект с случае успешной авторизации
  EsiaRequestParams requestDto = new EsiaRequestParams(API_URL);
  URI esiaAuthUri = new URIBuilder(ESIA_AUTH_URL)
          .addParameters(Arrays.asList(
            new BasicNameValuePair(RequestEnum.CLIENT_ID.getParam(), requestDto.getClientId()),
            new BasicNameValuePair(RequestEnum.SCOPE.getParam(), requestDto.getScope()),
            new BasicNameValuePair(RequestEnum.RESPONSE_TYPE.getParam(), requestDto.getResponseType()),
            new BasicNameValuePair(RequestEnum.STATE.getParam(), requestDto.getState()),
            new BasicNameValuePair(RequestEnum.TIMESTAMP.getParam(), requestDto.getTimestamp()),
            new BasicNameValuePair(RequestEnum.ACCESS_TYPE.getParam(), requestDto.getAccessType()),
            new BasicNameValuePair(RequestEnum.REDIRECT_URI.getParam(), requestDto.getRedirectUri()),
            new BasicNameValuePair(RequestEnum.CLIENT_SECRET.getParam(), requestDto.getClientSecret())
          ))
          .build();
  return Response.temporaryRedirect(esiaAuthUri).build();
}

Для наглядности покажем, как может выглядеть класс EsiaRequestParams:

public class EsiaRequestParams {

  String clientId;
  String scope;
  String responseType;
  String state;
  String timestamp;
  String accessType;
  String redirectUri;
  String clientSecret;
  String code;
  String error;
  String grantType;
  String tokenType;

  public EsiaRequestParams(String apiUrl) throws Exception {
    this.clientId = CLIENT_ID;
    this.scope = Arrays.stream(ScopeEnum.values())
            .map(ScopeEnum::getName)
            .collect(Collectors.joining(" "));
    responseType = RESPONSE_TYPE;
    state = EsiaUtil.getState();
    timestamp = EsiaUtil.getUrlTimestamp();
    accessType = ACCESS_TYPE;
    redirectUri = apiUrl   RESOURCE_URL   "/"   AUTH_REQUEST_ESIA;
    clientSecret = EsiaUtil.generateClientSecret(String.join("", scope, timestamp, clientId, state));
    grantType = GRANT_TYPE;
    tokenType = TOKEN_TYPE;
  }
}

Получение данных организации

Полученный токен доступа для запроса данных по организации мы использовать не можем, т.к. он завязан на определенный scope. Поэтому нужно получить отдельный токен. По сути методы получения токена доступа для организации и информации по ней не сильно отличаются от рассмотренных ранее.

Получение данных пользователя


В нашем случае необходимо получить ФИО, дату рождения, паспортные данные и контакты.

Используем функциональный интерфейс, который поможет получать данные пользователя:

Получение настроек подключения к есиа

Бывает полезно получить информацию о подключении к ЕСИА (адрес хоста, сертификат и т.д.) вне IEsiaRestService.

Это можно сделать путём запроса интерфейса IEsiaEnvironment.

Также открытыми являются классы TestEsiaEnvironment и ProductionEsiaEnvironment, от которых можно унаследоваться.

Пример использования настроек подключения смотрите в проекте EsiaSample на стартовой странице.

Получение токена доступа

Для получение каких-либо данных в ЕСИА нам нужно получить токен доступа. Для этого формируем POST запрос в ЕСИА (для тестовой среды базовый url такой:

Предисловие

Однажды в далекой-далекой галактике… потребовалось нам реализовать аутентификацию пользователей с помощью учетной записи ЕСИА на ГосУслугах. Т.к. обитаем мы в галактике .Net, первым делом был изучен весь гугол на предмет готового космолета дабы не костылить все самим, но поиски ни к чему путному не привели. Поэтому решено было изучить тему и реализовать-таки космолет своими силами.

Провайдер для подписания запроса к есиа

Провайдер обязателен. Должен реализовать интерфейс ISignProvider.

Существует DefaultSignProvider, который реализует подпись и проверку через сертификат клиент-системы ЕСИА.

Реализация

Устройство библиотеки тривиальное, комментариев не требует, после написания стало понятно, что можно было бы спроектировать и лучше, чтобы пользователю библиотеки требовалось выполнять меньше действий с ее интерфейсом.

Стоит отметить, что используется утилита openssl для подписи, в связи с этим есть лишняя операция создания временного файла.


Нас устраивает текущая реализация, но лучше было бы использовать pyopenssl.

У нас нет планов развития библиотеки, пока не будет требований в проекте, а их в ближайшей перспективе нет.Если используете esia-connector в ваших проектах и попутно что-то добавите/исправите — PR-те, будем рады включить.

Что можно было бы сделать:

  1. Реорганизовать интерфейс библиотеки для упрощения использования.
  2. Заменить использование openssl на pyopenssl.
  3. Разработать функционал по получению других данных из ЕСИА.
  4. Поддержать альтернативный протокол обмена данных реализованный в ЕСИА (SAML).
  5. Реализовать обертки для популярных фреймворков, например: Django, Flask, возможно в рамках отдельных проектов.

Требования

  1. AspNetCore не ниже 3.1.
  2. Алгоритм формирования электронной подписи должен быть RS256 (указывается в настройках ИС на технологическом портале).

Установка и использование

Необходимо установить Nuget-пакет EsiaNET.AspNetCore.Authentication:

Версия пакета 6.0.0 поддерживает .NET Core 3.1, .NET 5 и .NET 6.

Заключение

Пожалуй, этого достаточно для базового сценария взаимодействия с ЕСИА. В целом, если знать особенности реализации, программное подключение системы к ЕСИА не займет более 1 дня. Если у вас появятся вопросы, добро пожаловать в комменты. Спасибо, что дочитали мой пост до конца, надеюсь, он будет полезен.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *