Предисловие
Передо мной стояла задача по интеграции нашего сервиса с госуслугами. Казалось ничего сложного не предстоит, но учитывая что наш сервис базируется на технологии ASP.NET всё было не так оптимистично. В начале были поиски.. много поисков, которые привели к множеству разрозненной и чаще всего неактуальной информации. Так же были найдены уже готовые решения, но как заявляли некоторые товарищи на форумах за такое могут и по головке погладить. Поэтому было решено писать самому.
Эта статья скорее больше актуализация и дополнение информации из этой статьи.
Введение
На сайте Минцифр есть методичка максимально раздутая и очень запутанная, но пользоваться ею нам всё равно придётся. Мы будем работать с ЕСИА версии 3.11 (актуальная на момент написания статьи). Кратко наши действия заключаются вот в чем:
Регистрация ИС в регистре информационных систем ЕСИА
Регистрация ИС в тестовой среде
Выполнение доработки системы для взаимодействия с ЕСИА
Звучит довольно просто, но каждый шаг целая отдельная история приключений. Регистрация ИС в ЕСИА приключение для бюрократа. Поэтому в этой статье мы немного посмотрим на второй шаг, и детально распишем реализацию.
Содержание
Всё необходимое
Минцифры требуют использование сертифицированного ПО для криптографии. Поэтому мы будем использовать КриптоПРО CSP + КриптоПРО .Net + КриптоПРО .NetSDK. Всё это можно скачать с офф. сайта КриптоПРО. На время разработки лучше использовать триал версию.
Наш инвентарь для путешествия:
КриптоПРО CSP
КриптоПРО .Net
КриптоПРО .NetSDK
Контейнер закрытого ключа с сертификатом нашей организации
Много терпения
Немного о КриптоПРО CSP + .Net Core 5+
Вот тут и начинаются первые проблемы. На момент написания статьи у КриптоПРО .Net нет поддержки .Net Core 5 и выше. Есть сборка под .Net Core 3.1 но и она выглядит сомнительно. Поэтому было решено поднять сервис для .Net Framework 4.8 который будет использовать средства КриптоПРО CSP для подписания с использованием ЭЦП, а так же проверки ответов от ЕСИА.
Немного о контейнере закрытого ключа и сертификата
Когда мы начинали делать эту задачу у нас была КЭП на токене, но как оказалось на нём был неэкспортируемый контейнер. Скажу сразу, что экспортировать контейнер с такого токена запрещено ФНС. Поэтому необходимо заранее получить токен на имя сотрудника с экспортируемым контейнером. Так как его необходимо будет скопировать на сервер.
Приступаем
Начнём с того, что вы уже отправили заявку регистрации ИС в ЕСИА и её приняли. А так же отправили заявка на тестовую среду. Приступим к этапу настройки ИС в тестовом кабинете электронного правительства. Вот ссылка на тестовую страницу. Логинимся под тестовой учетной записью тестового пользователя 006(все данные лежат в приложении к работе с тестовой средой), так как он имеет доступ к управлением ИС.
Здесь ищем нашу систему по Мнемонике или полному названию, если таковой нет то создаём. Напротив нашей системы есть две кнопки:
Первая кнопка - изменить нашу ИС (информация о ИС, редиректы и тд)
Вторая кнопка - наши сертификаты с помощью которых мы подписываем сообщения в ЕСИА
Настройка ИС
Есть важный момент в настройки ИС. Это URL системы. Тут мы указываем ссылки куда ЕСИА может делать переадресацию при запросе от нашей ИС. На эти точки будет приходить авторизационный код (Если он указан в запросе).
Сертификаты ИС
Здесь мы можем загрузить наш сертификаты или же удалить их. Есть один важный момент, каждая ИС может иметь только один уникальный сертификат. А связи с тем, что на тестовой среде все системы регистрируются под одним пользователем и сертификаты тестовые одни на всех часта такая ситуация, что кто-то удаляет у вас сертификат и загружает к себе. А ваши запросы теперь падают с ошибкой) Но если у вас уже готов ЭЦП на сотудника, то лучше используйте её.
Реализуем
Мы закончили с настройки нашей ИС и можем приступить к реализации. Надеюсь вы уже установили КриптоПРО и всё необходимое для него. Если нет, я подожду...
Устанавливаем сертификаты
Такс~ Всё готово. Качаем сертификаты по ссылке из методички. Специально не буду вставлять, так как может измениться.
Здесь нам интересен сертификат ТЕСИА ГОСТ 2012.cer - это сертификат с помощью которого ЕСИА подписывает сообщения отправляя в нашу ИС. (Соответственно для продуктовой среды свой сертификат). Устанавливаем сертификат как доверенный. Здесь ничего сложного думаю разберётесь.
Теперь устанавливаем тестовый контейнер и сертификат. Для примера будем использовать предоставленные ЕСИА контейнеры, но вы можете использовать свои. Всё это лежит внутри архива.
В архиве лежит папка d1f73ca5.000 - это контейнер нам необходимо его переместить по пути C:\Users\User\AppData\Local\Crypto Pro
Теперь открываем КриптоПРО CSP. Выбираем установить личный сертификат и указываем Тестовое ведомство Фамилия006 ИО.cer и нажимаем найти автоматически. Выполняем оставшиеся шаги сами.
Механизм подписания
Пожалуй начинается самая важная и самая запутанная часть всего пути. Здесь мы реализуем сервис для работы с подписью. И так делаю выжимку из методических материалов, чтобы Вам не пришлось читать много текста.
Для получения авторизационный ссылки - ссылка на которую мы будем переадресовывать пользователя для авторизации в ЕСИА. Нам необходимо собрать ссылку из параметров.
client_id
- наша Мнемоникаclient_secret
- Отсоединённая подпись от параметров запроса в кодировке UTF-8redirect_uri
- ссылка на которую ЕСИА будет переадресовывать пользователя вместе с авторизационным кодомscope
- перечень запрашиваемой информации. Напримерfullname birthdate gender
response_type
- тип ответа от ЕСИА, в нашем случае это просто строчкаcode
state
- Идентификатор текущего запроса. Генерируется таким образомGuid.NewGuid().ToString("D");
timestamp
- время запроса авторизационного кода в формате yyyy.MM.dd HH:mm:ss Z. Генерируется таким образомDateTime.UtcNow.ToString("yyyy.MM.dd HH:mm:ss +0000");
client_certificate_hash
- это fingerprint сертификата в HEX-формате.
Обозначили наш зоопарк. Самый важный зверь здесь client_secret
Получаем client_certificate_hash
В методическом указании от Минцифр есть ссылка на специальную утилиту с помощью которой мы можем получить этот хэш. Разархивировали архив и видим перед нами sh. Windows пользователи не пугаемся, на самом деле тут же лежит .exe файл. Чтобы вычислить хэш нашего сертификат просто необходимо из cmd запустить вот такой скрипт:
cpverify.exe test.cer -mk -alg GR3411_2012_256 -inverted_halfbytes 0
Формирование client_secret
Такс перед тем как просто получит client_secret
нам необходимо сделать:
ASP.Net Framework 4.8 WebAPI - тот самый сервис который будет работать с КриптоПРО CSP
Пропустим множество шагов создания этого сервиса и перейдём сразу к его настройки для работы с КриптоПРО CSP.
Настройка сервиса для работы с КриптоПРО CSP
Добавляем ссылки на DLL КриптоПРО.
Переходим по пути C:\Program Files (x86)\Crypto Pro.NET SDK\Assemblies\4.0
Выбираем всё что нам нужно. (подробная информация)
Теперь мы имеем доступ к API КриптоПРО CSP из кода .Net Framework
Теперь создаём контроллер:
Код контроллера
Итак нам необходимо получать строку для подписания. Создадим метод
const string CertSerialNumber = "01f290e7008caed0904b967783fd0e4ad6";
const string EsiaCertSerialNumber = "0125657e00a1ae59804d92116214e53466";
[HttpGet]
public string Get(string msg)
{
msg = Base64UrlEncoder.Decode(msg);
var data = Encoding.UTF8.GetBytes(msg);
var client_secret = Sign(data);
return client_secret;
}
Мы заранее укажем константами серийные номера сертификатов.
В методе Get получаем строку в Base64Url формате, чтобы спокойно передавать наши длинные сообщения.
Декодируем строку из Base64Url в текст. После чего переводим текст в байты используя UTF-8. А теперь подписываем.
string Sign(byte[] data)
{
var gost3411 = new Gost3411_2012_256CryptoServiceProvider();
var hashValue = gost3411.ComputeHash(data);
gost3411.Clear();
var signerCert = GetSignerCert();
var SignedHashValue = GostSignHash(hashValue,
signerCert.PrivateKey as Gost3410_2012_256CryptoServiceProvider, "Gost3411_2012_256");
var client_secret = Base64UrlEncoder.Encode(SignedHashValue);
return client_secret;
}
И так что мы тут делаем. С помощью ГОСТ 34.11-2012 мы вычисляем хэш нашего сообщения. И используя полученный сертификат подписываем сообщение.
X509Certificate2 GetSignerCert()
{
var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
var certificates = store.Certificates.Find(X509FindType.FindBySerialNumber, CertSerialNumber, false);
if (certificates.Count != 1)
{
return null;
}
var certificate = certificates[0];
if (certificate.PrivateKey == null)
{
return null;
}
return certificate;
}
Здесь мы открываем наш склад с контейнерами и ищем именно тот где лежит наш сертификат. После чего извлекаем из него сертификат.
byte[] GostSignHash(byte[] HashToSign, Gost3410_2012_256CryptoServiceProvider key, string HashAlg)
{
try
{
//Создаем форматтер подписи с закрытым ключом из переданного
//функции криптопровайдера.
var Formatter = new Gost2012_256SignatureFormatter(
(Gost3410_2012_256CryptoServiceProvider) key);
//Устанавливаем хэш-алгоритм.
Formatter.SetHashAlgorithm(HashAlg);
//Создаем подпись для HashValue и возвращаем ее.
return Formatter.CreateSignature(HashToSign);
}
catch (CryptographicException e)
{
Console.WriteLine(e.Message);
return null;
}
}
С помощью этого кода как раз и создаётся наша подпись на хэш строки. Здесь используется ГОСТ 34.10-2012.
Итак контроллер готов. Теперь переходим в наш основной проект на .Net Core
Создаём строку подписания. Просто выполняем конкатенацию параметры без разделителей. Здесь я использую IOptions чтобы брать параметры из appsettings.json.
var msg = $"{esiaSettings.Value.ClientId}{esiaSettings.Value.Scope}{timestamp}{state}{redirectUri}";
Мы получил строку для подписания. Теперь нам необходимо эту строку закодировать в Base64Url и отправляем её на подписание в написанный нами заранее сервис
private string GetClientSecret(string msg){
var client = new HttpClient();
var msgBase64 = Base64UrlEncoder.Encode(msg);
var response = await client.GetAsync($"{cryptoProSettings.Value.BaseUrl}/Get?msg={msgBase64}");
var clientSecret = await response.Content.ReadAsStringAsync();
clientSecret = JsonConvert.DeserializeObject<string>(clientSecret);
return clientSecret;
}
Собираем ссылку для авторизации в Госуслугах
Наконец-то мы получили этот долгожданный секрет. Но вы могли бы подумать это всё, дальше всё просто и ясно. Не тут то было! Дело в том, что ЕСИА требует Base64 Url Safe кодироку. И она немного отличается от Base64Url кодировки доступной из коробки .Net
Итак дело за малым, собираем нашего гомункула из секрета и параметров.
Класс помощник для сборки ссылки
Возможно излишне, но мне понравился метод сбора вот таким способом.
public class RequestBuilder
{
List<RequesItemClass> items = new List<RequesItemClass>();
public void AddParam(string name, string value)
{
items.Add(new RequesItemClass { name = name, value = value });
}
public override string ToString()
{
return string.Join("&", items.Select(a => a.name + "=" + a.value));
}
}
public class RequesItemClass
{
public string name;
public string value;
}
Код сборки ссылки
async Task<string> UrlBuild(string redirectUri)
{
using var client = new HttpClient();
var timestamp = DateTime.UtcNow.ToString("yyyy.MM.dd HH:mm:ss +0000");
var state = Guid.NewGuid().ToString("D");
var msg = $"{esiaSettings.Value.ClientId}{esiaSettings.Value.Scope}{timestamp}{state}{redirectUri}";
var clientSecret = await GetClientSecret(msg);
var builder = new RequestBuilder();
builder.AddParam("client_secret", clientSecret);
builder.AddParam("client_id", esiaSettings.Value.ClientId);
builder.AddParam("scope", esiaSettings.Value.Scope);
builder.AddParam("timestamp", timestamp);
builder.AddParam("state", state);
builder.AddParam("redirect_uri", redirectUri);
builder.AddParam("client_certificate_hash", esiaSettings.Value.ClientCertificateHash);
builder.AddParam("response_type", "code");
builder.AddParam("access_type", "online");
//Вот тут самый важный момент на который было потрачено множество времени. Просто заменяем символы на безопасные
var url = esiaSettings.Value.EsiaAuthUrl + "?" + builder.ToString().Replace("+", "%2B")
.Replace(":", "%3A")
.Replace(" ", "+");
return url;
}
Получаем ссылку на подобии вот такой:
Здесь https://esia-portal1.test.gosuslugi.ru/aas/oauth2/v2/ac
ссылка на конечную точку получения авторизационно кода, указана в методическом материале.
https://esia-portal1.test.gosuslugi.ru/aas/oauth2/v2/ac?client_secret=v_c33_-LpkyKJbopTEYqBMbGZrBy9r9u1pzbRmMLNlJPcBnPTJj6Xx5DuxXba3EZZoXdMsb0YIwPDCoF0dfYjQ&client_id=MEMONIKA&scope=fullname+birthdate+gender×tamp=2022.12.23+16%3A37%3A45+%2B0000&state=3a19c4d7-594b-496f-aa6e-970c75a925a4&redirect_uri=https%3A//api.site/users/esia&client_certificate_hash=EED1079A4FF154E117EAA196DCB551930807825DE1DE15EAF7607F354BA47423&response_type=code&access_type=online
Теперь перенаправляем пользователя по этой ссылке и ожидаем пока он авторизуется. После авторизации ЕСИА переадресует его на нашу ссылку и отправит туда в виде аргументов авторизационный код и state.
Получение токена доступа
Теперь время получить токен взамен на авторизационный код.
Метод для получение токена
public async Task<EsiaAuthToken> GetToken(string authorizationCode, string redirectUrl)
{
var timestamp = DateTime.UtcNow.ToString("yyyy.MM.dd HH:mm:ss +0000");
var state = Guid.NewGuid().ToString("D");
var msg =
$"{esiaSettings.Value.ClientId}{esiaSettings.Value.Scope}{timestamp}{state}{redirectUrl}{authorizationCode}";
var clientSecret = await GetClientSecret(msg);
var requestParams = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("client_id", esiaSettings.Value.ClientId),
new KeyValuePair<string, string>("code", authorizationCode), //Здесь мы передаём полученный код
new KeyValuePair<string, string>("grant_type", "authorization_code"), //Просто указываем тип
new KeyValuePair<string, string>("state", state),
new KeyValuePair<string, string>("scope", esiaSettings.Value.Scope),
new KeyValuePair<string, string>("timestamp", timestamp),
new KeyValuePair<string, string>("token_type", "Bearer"), //Какой токен мы хотим получить
new KeyValuePair<string, string>("client_secret", clientSecret),
new KeyValuePair<string, string>("redirect_uri", redirectUrl),
new KeyValuePair<string, string>("client_certificate_hash", esiaSettings.Value.ClientCertificateHash)
};
using var client = new HttpClient();
using var response = await client.PostAsync(esiaSettings.Value.EsiaTokenUrl,
new FormUrlEncodedContent(requestParams));
response.EnsureSuccessStatusCode();
var tokenResponse = await response.Content.ReadAsStringAsync();
var token = JsonConvert.DeserializeObject<EsiaAuthToken>(tokenResponse);
if (!await ValidatingAccessToken(token))
{
throw new Exception("Ошибка проверки маркера индентификации");
}
return token;
}
Тут всё простенько, снова генерируем client_secret
указываем остальные параметры и отправляем запрос в ЕСИА на получение токена. Тестовый Uri https://esia-portal1.test.gosuslugi.ru/aas/oauth2/v3/te
Класс токена
public class EsiaAuthToken
{
/// <summary>
/// Токен доступа
/// </summary>
[JsonProperty("access_token")]
public string AccessToken { get; set; }
/// <summary>
/// Идентификатор запроса
/// </summary>
public string State { get; set; }
string[] parts => AccessToken.Split('.');
/// <summary>
/// Хранилище данных в токене
/// </summary>
public EsiaAuthTokenPayload Payload
{
get
{
if (string.IsNullOrEmpty(AccessToken))
{
return null;
}
if (parts.Length < 2)
{
throw new Exception($"При расшифровке токена доступа произошла ошибка. Токен: {AccessToken}");
}
var payload = Encoding.UTF8.GetString(Base64UrlEncoder.DecodeBytes(parts[1]));
return JsonConvert.DeserializeObject<EsiaAuthTokenPayload>(payload);
}
}
/// <summary>
/// Сообщение для проверки подписи
/// </summary>
[Newtonsoft.Json.JsonIgnore]
public string Message
{
get
{
if (string.IsNullOrEmpty(AccessToken))
{
return null;
}
if (parts.Length < 2)
{
throw new Exception($"При расшифровке токена доступа произошла ошибка. Токен: {AccessToken}");
}
return parts[0] + "." + parts[1];
}
}
/// <summary>
/// Сигнатура подписи
/// </summary>
[Newtonsoft.Json.JsonIgnore]
public string Signature
{
get
{
if (string.IsNullOrEmpty(AccessToken))
{
return null;
}
if (parts.Length < 2)
{
throw new Exception($"При расшифровке токена доступа произошла ошибка. Токен: {AccessToken}");
}
return parts[2];
}
}
public class EsiaAuthTokenPayload
{
[JsonConstructor]
public EsiaAuthTokenPayload(string tokenId, string userId, string nbf, string exp, string iat, string iss,
string client_id)
{
TokenId = tokenId;
UserId = userId;
BeginDate = EsiaHelper.DateFromUnixSeconds(double.Parse(nbf));
ExpireDate = EsiaHelper.DateFromUnixSeconds(double.Parse(exp));
CreateDate = EsiaHelper.DateFromUnixSeconds(double.Parse(iat));
Iss = iss;
ClientId = client_id;
}
/// <summary>
/// Идентификатор токена
/// </summary>
[JsonProperty("urn:esia:sid")]
public string TokenId { get; private set; }
/// <summary>
/// Идентификатор пользователя
/// </summary>
[JsonProperty("urn:esia:sbj_id")]
public string UserId { get; private set; }
/// <summary>
/// Время начала действия токена
/// </summary>
[JsonPropertyName("nbf")]
public DateTime BeginDate { get; private set; }
/// <summary>
/// Время окончания действия токена
/// </summary>
[JsonPropertyName("exp")]
public DateTime ExpireDate { get; private set; }
/// <summary>
/// Время выпуска токена
/// </summary>
[JsonPropertyName("iat")]
public DateTime CreateDate { get; private set; }
/// <summary>
/// Организация, выпустившая маркер
/// </summary>
[JsonPropertyName("iss")]
public string Iss { get; private set; }
/// <summary>
/// Адресат маркера
/// </summary>
[JsonPropertyName("client_id")]
public string ClientId { get; private set; }
}
}
public static class EsiaHelper
{
public static DateTime DateFromUnixSeconds(double seconds)
{
var date = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
return date.AddSeconds(seconds).ToLocalTime();
}
}
Проверка токена
Итак помимо того, что нам нужно получить токен, нам так же необходимо проверить его.
Сам токен состоит из 3 частей.
1 часть - заголовок JWT токена
2 часть - payload токена, там вся основная информация о токене
3 часть - RAW подпись в формате UTF-8
Код конечной точки для проверки подписи
[HttpPost]
public bool Verify(VerifyMessage message)
{
try
{
return VerifyRawSignString(message.Message, message.Signature);
}
catch (Exception ex)
{
return false;
}
}
public class VerifyMessage
{
public string Signature { get; set; }
public string Message { get; set; }
}
Код проверки подписи на нашем сервисе
/// <summary>
/// Проверка подписи JWT в формате HEADER.PAYLOAD.SIGNATURE.
/// </summary>
/// <param name="message">HEADER.PAYLOAD в формате Base64url</param>
/// <param name="signature">SIGNATURE в формате Base64url</param>
bool VerifyRawSignString(string message, string signature)
{
var signerCert = GetEsiaSignerCert();
var messageBytes = Encoding.UTF8.GetBytes(message);
var signatureBytes = Base64UrlEncoder.DecodeBytes(signature);
//Переварачиваем байты, так как используется RAW подпись
Array.Reverse(signatureBytes, 0, signatureBytes.Length);
using (var GostHash = new Gost3411_2012_256CryptoServiceProvider())
{
var csp = (Gost3410_2012_256CryptoServiceProvider) signerCert.PublicKey.Key;
//Используем публичный ключ сертификата для проверки
return csp.VerifyData(messageBytes, GostHash, signatureBytes);
}
}
Код получения сертификата ЕСИА
X509Certificate2 GetEsiaSignerCert()
{
var store = new X509Store(StoreName.AddressBook, StoreLocation.CurrentUser);
store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
var certificates = store.Certificates.Find(X509FindType.FindBySerialNumber, EsiaCertSerialNumber, false);
var certificate = certificates[0];
return certificate;
}
Здесь используем введённые ранее константы. И Получаем сертификат из доверенных сертификатов.
Отправка токена на проверку
public async Task<bool> ValidatingAccessToken(EsiaAuthToken token)
{
if (token.Payload.ExpireDate <= DateTime.Now ||
token.Payload.BeginDate >= DateTime.Now ||
token.Payload.CreateDate >= DateTime.Now ||
token.Payload.ExpireDate <= token.Payload.BeginDate ||
token.Payload.CreateDate > token.Payload.BeginDate ||
token.Payload.CreateDate > token.Payload.ExpireDate ||
token.Payload.Iss != esiaSettings.Value.ISS ||
token.Payload.ClientId != esiaSettings.Value.ClientId)
{
return false;
}
var client = new HttpClient();
var requestParams = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("signature", token.Signature),
new KeyValuePair<string, string>("message", token.Message)
};
var response = await client.PostAsync($"{cryptoProSettings.Value.BaseUrl}/Verify",
new FormUrlEncodedContent(requestParams));
response.EnsureSuccessStatusCode();
var resultResponse = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<bool>(resultResponse);
return result;
}
Этот код используем в нашем основном сервисе.
Проверяем поля токена на актуальность, чтобы его не могли подделать. А потом уже проверяем подпись токена, как указано в методических указаниях.
Получение данных пользователя из ЕСИА
Имея токен мы может отправить запрос на получение данных о пользователе указанных в scope токена. Пример кода, где мы получаем данные пользователя. Здесь esiaUserId содержится в самом токене, это уникальный идентификатор пользователя ЕСИА. Наш токен указываем в заголовке авторизации.
public async Task<EsiaUser> ExecuteAsync(string esiaUserId, string accessToken)
{
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var response = await client.GetStringAsync($"{esiaSettings.Value.EsiaRestUrl}/prns/{esiaUserId}");
var user = JsonConvert.DeserializeObject<EsiaUser>(response);
user.Id = user.Id ?? esiaUserId;
return user;
}
}
Код класса EsiaUser
public class EsiaUser
{
/// <summary>
/// Идентификатор
/// </summary>
[JsonProperty("oid")]
public string Id { get; set; }
/// <summary>
/// Фамилия
/// </summary>
[JsonProperty("firstName")]
public string FirstName { get; set; }
/// <summary>
/// Имя
/// </summary>
[JsonProperty("lastName")]
public string LastName { get; set; }
/// <summary>
/// Отчество
/// </summary>
[JsonProperty("middleName")]
public string MiddleName { get; set; }
/// <summary>
/// Дата рождения
/// </summary>
[JsonProperty("birthdate")]
public string Birthdate { get; set; }
/// <summary>
/// Пол
/// </summary>
[JsonProperty("gender")]
public string Gender { get; set; }
/// <summary>
/// Подтвержден ли пользователь
/// </summary>
[JsonProperty("trusted")]
public bool Trusted { get; set; }
}
Заключение
Наконец мы закончили интеграцию с ЕСИА. Это был длинный путь полный странных вещей. Неясных решений и множество потраченного времени. Надеюсь этой статьёй я помог Вам реализовать задачу интеграции гораздо быстрее и легче. Спасибо за потраченное время.