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

Интеграция с ЕСИА v2 на Debian 11 + php 7

Передо мной была поставлена задача «чтобы посетители могли войти на сайт через Госуслуги». Задачка не новая, давно решена. На PHP для этого используют библиотеки https://github.com/fr05t1k/esia и https://github.com/ekapusta/oauth2-esia, но есть пара оговорок.

Проблема 1

С января 2020 для интеграции требуется использовать только ГОСТ-шифрование. Варианты решения:

Оба варианта требуют применения лицензированного криптографического средства (КриптоПро), в рамках статьи мы пойдем по второму пути. Весь php можно не пересобирать, ведь есть механизм плагинов. Поэтому достаточно собрать плагин libphpcades от КриптоПро.

Для Debian 11 и php 7.4 инструкцию от КриптоПро мне пришлось чуть доработать напильником. Шаги 2 и 6 пропускаем, достаточно лишь установить пакет php-dev. После этого на шаге 7 в Makefile.unix можно задать путь к заголовочным файлам из пакета php-dev

Далее на шаге 8 скачиваем патч для php7 и применяем его к исходным файлам libphpcades :

Шаг «8 и 3/4 «: Чтобы библиотека собралась под debian 11, добавляем к параметрам компилятора CFLAGS флаг

А из параметров сборки LDFLAGS удаляем флаг

И после этого библиотека успешно собирается. По-хорошему, теперь собранную libphpcades.so надо бы оформить в пакет deb.

Так что на шаге 11 вместо создания симлинка мы просто копируем собранную библиотеку в компанию к остальным модулям php

cp /opt/cprocsp/src/phpcades/libphpcades.so /usr/lib/php/20190902/

После чего исходные файлы нам уже не нужны, их можно удалить.

Для тестового контура ЕСИА ГОСТ-сертификат можно сгенерировать самостоятельно, для промышленного — придется получить в уполномоченном Удостоверяющем центре (ОГРН в сертификате должен совпадать с ОГРН вашей ИС в промышленном контуре ЕСИА)

Любым способом переносим ключ и сертификат нашей ИС в /var/opt/cprocsp/keys/www-data/MYSITE.000 , можно простым копированием, можно командой

sudo -u www-data /opt/cprocsp/bin/amd64/csptest -keycopy -contsrc ‘\\.\FLASH\ivanoff’ -contdest ‘\\.\HDIMAGE\MYSITE’

Далее устанавливаем сертификат и его закрытый ключ в хранилище My пользователя www-data:

sudo -u www-data /opt/cprocsp/bin/amd64/certmgr -inst -store uMy -cont ‘\\.\HDIMAGE\MYSITE’

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

PrivateKey Link : Yes
Container : \\.\HDIMAGE\MYSITE

Устанавливаем открытый сертификат ЕСИА в контейнер Users пользователя www-data

sudo -u www-data /opt/cprocsp/bin/amd64/certmgr -install -store uUsers -file ./TESIA\ GOST\ 2012.cer
sudo -u www-data /opt/cprocsp/bin/amd64/certmgr -install -store uUsers -file ./GOST\ 2012\ PROD.cer

Проблема 2

С версии 2.90 Методических рекомендаций используемый endpoint /aas/oauth2/ac объявлен устаревшим и не рекомендован к использованию. Предложено переходить на /aas/oauth2/v2/ac, а реализации в указанных библиотеках нет.

Вот этим‑то мы сейчас и займемся. Снова достаем напильник и применяем его ну например к библиотечке https://github.com/fr05t1k/esia

Добавляем новые свойства конфигурации:

class Config < private $tokenUrlPath_V3 = 'aas/oauth2/v3/te'; private $codeUrlPath_V2 = 'aas/oauth2/v2/ac'; protected $ESIACertSHA1 = '01e6041097ccf5a26da1d75fdbb1e7aaee07bd2a'; // SHA1 хэш сертификата ЕСИА (openssl sha1 ./GOST\ 2012\ PROD.cer) public function getTokenUrl_V3(): string < return $this->portalUrl . $this->tokenUrlPath_V3; >

На основе метода buildUrl() создаем метод buildUrl_V2() , который будет работать с endpoint из $codeUrlPath_V2

В endpoint «v2/ac» поменялся состав и порядок конкатенации параметров для составления client_secret (теперь это ClientID + Scope + Timestamp +State + RedirectURI) и добавился client_certificate_hash.

class OpenId < protected $clientCertHash = null; public function buildUrl_V2() < $timestamp = $this->getTimeStamp(); $state = $this->buildState(); // собираем client_secret по новым правилам $message = $this->config->getClientId() . $this->config->getScopeString() . $timestamp . $state . $this->config->getRedirectUrl(); // используем алгоритм ГОСТ2012 для подписания $this->signer = new SignerCPDataHash( $config->getCertPath(), // для КриптоПро эти $config->getPrivateKeyPath(), // параметры не нужны $config->getPrivateKeyPassword(), // потому что сертификат и ключ $config->getTmpPath() // импортированы в хранилище ); $clientSecret = $this->signer->sign($message); $url = $this->config->getCodeUrl_V2() . '?%s'; $params = [ 'client_id' => $this->config->getClientId(), 'client_secret' => $clientSecret, 'redirect_uri' => $this->config->getRedirectUrl(), 'scope' => $this->config->getScopeString(), 'response_type' => $this->config->getResponseType(), 'state' => $state, 'access_type' => $this->config->getAccessType(), 'timestamp' => $timestamp, 'client_certificate_hash' => $this->clientCertHash, // ГОСТ-хэш нашего сертификата ]; $request = http_build_query($params); return sprintf($url, $request); >

Да, client_certificate_hash можно вычислить предлагаемой в Методических рекомендациях утилитой и вписать как еще один параметр конфигурации. Возможно, так даже правильно. Но почему бы не научиться вычислять его самостоятельно?

Итак, создаем класс SignerCPDataHash, который использует libphpcades для вычисления ГОСТ хэша сертификата ИС (параметр client_certificate_hash ) и подписания client_secret по алгоритму data hash.

Для этого используется сертификат с CN=ClientID из пользовательского хранилища My установленного КриптоПро.

class SignerCPDataHash extends AbstractSignerPKCS7 implements SignerInterface < public function sign(string $message): string < $store = new \CPStore(); $store->Open(CURRENT_USER_STORE, 'My', STORE_OPEN_READ_ONLY); // используем хранилище My текущего пользователя (www-data) $certs = $store->get_Certificates(); $certlist = $certs->Find(CERTIFICATE_FIND_SUBJECT_NAME, $this->config->getClientId(), 0); // ищем сертификат, у которогое Subject = мнемонике нашей ИС $cert = $certlist->Item(1); if (!$cert) < throw new CannotReadCertificateException('Cannot read the certificate'); >// у сертификата должна быть связка с закрытым ключом if (false===$cert->HasPrivateKey()) < throw new CannotReadPrivateKeyException('Cannot read the private key'); >$pk=$cert->PublicKey(); $oid=$pk->get_Algorithm(); $hd = new \CPHashedData(); switch ($oid->get_Value()) < // https://cpdn.cryptopro.ru/content/csp40/html/group___pro_c_s_p_ex_DP8.html case '1.2.643.7.1.1.1.1': $hd->set_Algorithm(CADESCOM_HASH_ALGORITHM_CP_GOST_3411_2012_256); break; case '1.2.643.7.1.1.1.2': $hd->set_Algorithm(CADESCOM_HASH_ALGORITHM_CP_GOST_3411_2012_512); break; > $hd->set_DataEncoding(BASE64_TO_BINARY); $hd->Hash($cert->Export(ENCODE_BASE64)); $this->clientCertHash=$hd->get_Value(); //получили ГОСТ хэш нашего сертификата unset($hd); $hd = new \CPHashedData(); $hd->set_Algorithm(CADESCOM_HASH_ALGORITHM_CP_GOST_3411_2012_256); $hd->set_DataEncoding(BASE64_TO_BINARY); $hd->Hash(base64_encode($message)); // посчитали ГОСТ хэш для $message $rs = new \CPRawSignature(); $shash=$rs->SignHash($hd, $cert); // https://docs.cryptopro.ru/cades/reference/cadescom/cadescom_interface/irawsignaturesignhash : Подпись для ключей ГОСТ Р 34.10-2001 возвращается как описано в разделе 2.2.2 RFC 4491 (http://tools.ietf.org/html/rfc4491#section-2.2.2), но в обратном порядке байт. $signed=base64_encode(strrev(hex2bin($shash))); // получили подписанный data hash $sign = str_replace("\n", '', $this->urlSafe($signed)); return $sign; > > 

Теперь займемся получением и проверкой JWT токена:

 public function getToken_V3(string $code): string < $timestamp = $this->getTimeStamp(); $state = $this->buildState(); $this->signer = new SignerCPDataHash( $config->getCertPath(), $config->getPrivateKeyPath(), $config->getPrivateKeyPassword(), $config->getTmpPath() ); $clientSecret = $this->signer->sign( $this->config->getClientId() . $this->config->getScopeString() . $timestamp . $state . $this->config->getRedirectUrl(); ); $body = [ 'client_id' => $this->config->getClientId(), 'code' => $code, 'grant_type' => 'authorization_code', 'client_secret' => $clientSecret, 'state' => $state, 'redirect_uri' => $this->config->getRedirectUrl(), 'scope' => $this->config->getScopeString(), 'timestamp' => $timestamp, 'token_type' => 'Bearer', 'refresh_token' => $state, 'client_certificate_hash' => $this->clientCertHash, ]; $payload = $this->sendRequest( new Request( 'POST', $this->config->getTokenUrl_V3(), [ 'Content-Type' => 'application/x-www-form-urlencoded', ], http_build_query($body) ) ); $this->logger->debug('Payload: ', $payload); $token = $payload['access_token']; $chunks = explode('.', $token); $payload = json_decode($this->base64UrlSafeDecode($chunks[1]), true); $header = json_decode($this->base64UrlSafeDecode($chunks[0]), true); $_token_signature = $this->base64UrlSafeDecode($chunks[2]); if ('JWT'==$header->typ) < $store = new \CPStore(); $store->Open(CURRENT_USER_STORE, "Users", STORE_OPEN_READ_ONLY); // используем хранилище Users $certs = $store->get_Certificates(); $certlist = $certs->Find(CERTIFICATE_FIND_SHA1_HASH, $this->ESIACertSHA1, 0); // ищем в хранилище сертификат ЕСИА по его sha1 хэшу $cert = $certlist->Item(1); if (!$cert) < throw new CannotReadCertificateException('Cannot read the certificate'); >$hd = new \CPHashedData(); $hd->set_DataEncoding(BASE64_TO_BINARY); switch ($header->alg) < case 'GOST3410_2012_256': $hd->set_Algorithm(CADESCOM_HASH_ALGORITHM_CP_GOST_3411_2012_256); break; case 'GOST3410_2012_512': $hd->set_Algorithm(CADESCOM_HASH_ALGORITHM_CP_GOST_3411_2012_512); break; default: throw new Exception('Invalid signature algorithm'); > $hd->Hash(base64_encode($chunks[0].'.'.$chunks[1])); $rs = new \CPRawSignature(); $rs->VerifyHash($hd, bin2hex(strrev($_token_signature)), $cert); //если попали на эту строчку, значит подпись валидная. Иначе бы уже было вызвано исключение. $this->config->setOid($payload['urn:esia:sbj_id']); $this->config->setToken($token); > // JWT token return $token; >

Ура. У нас теперь есть функции для работы с новыми endpoint ЕСИА: buildUrl_V2() и getToken_V3().

Источник

Saved searches

Use saved searches to filter your results more quickly

You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session. You switched accounts on another tab or window. Reload to refresh your session.

Компонент для авторизации на портале «Госуслуги» (ЕСИА)

fr05t1k/esia

This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?

Sign In Required

Please sign in to use Codespaces.

Launching GitHub Desktop

If nothing happens, download GitHub Desktop and try again.

Launching GitHub Desktop

If nothing happens, download GitHub Desktop and try again.

Launching Xcode

If nothing happens, download Xcode and try again.

Launching Visual Studio Code

Your codespace will open once ready.

There was a problem preparing your codespace, please try again.

Latest commit

Git stats

Files

Failed to load latest commit information.

README.md

Единая система идентификации и аутентификации (ЕСИА) OpenId

Компонент для авторизации на портале «Госуслуги».

Получив токен вы можете выполнять любые API запросы. Библиотека не поддерживает все существующие методы в API, а предоставляет только самые базовые. Основная цель библиотеки — получение токена.

composer require --prefer-dist fr05t1k/esia 

Или добавьте в composer.json

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

 $config = new \Esia\Config([ 'clientId' => 'INSP03211', 'redirectUrl' => 'http://my-site.com/response.php', 'portalUrl' => 'https://esia-portal1.test.gosuslugi.ru/', 'scope' => ['fullname', 'birthdate'], ]); $esia = new \Esia\OpenId($config); $esia->setSigner(new \Esia\Signer\SignerPKCS7( 'my-site.com.pem', 'my-site.com.pem', 'password', '/tmp' )); ?> a href pl-ent">$esia->buildUrl()?>">Войти через портал госуслугиa>

После редиректа на ваш redirectUrl вы получите в $_GET[‘code’] код для получения токена

Пример получения токена и информации о пользователе

$esia = new \Esia\OpenId($config); // Вы можете использовать токен в дальнейшем вместе с oid $token = $esia->getToken($_GET['code']); $personInfo = $esia->getPersonInfo(); $addressInfo = $esia->getAddressInfo(); $contactInfo = $esia->getContactInfo(); $documentInfo = $esia->getDocInfo();

clientId — ID вашего приложения.

redirectUrl — URL куда будет перенаправлен ответ с кодом.

portalUrl — по умолчанию: https://esia-portal1.test.gosuslugi.ru/ . Домен портала для авторизация (только домен).

codeUrlPath — по умолчанию: aas/oauth2/ac . URL для получения кода.

tokenUrlPath — по умолчанию: aas/oauth2/te . URL для получение токена.

scope — по умолчанию: fullname birthdate gender email mobile id_doc snils inn . Запрашиваемые права у пользователя.

privateKeyPath — путь до приватного ключа.

privateKeyPassword — пароль от приватного ключа.

certPath — путь до сертификата.

tmpPath — путь до дериктории где будет проходить подпись (должна быть доступна для записи).

Токен — jwt токен которые вы получаете от ЕСИА для дальнейшего взаимодействия

oid — уникальный идентификатор владельца токена

  1. oid содержится в jwt токене, расшифровав его
  2. После получения токена oid сохраняется в config и получить можно так

Дополнительно укажите токен и идентификатор в конфиге

$config->setToken($jwt); $config->setOid($oid);

About

Компонент для авторизации на портале «Госуслуги» (ЕСИА)

Источник

Читайте также:  Javascript document this element
Оцените статью