ЕСИА OpenID
PHP-компонент для авторизации через Единую систему идентификации и аутентификации (ЕСИА) портала «Госуслуги».
⚠️ Обновляйтесь на свой страх и риск. У авторов и контрибьюторов не всегда есть доступ к реальной среде ЕСИА: и промышленный контур, и тестовый стенд
esia-portal1.test.gosuslugi.ruдоступны только из РФ и требуют зарегистрированной ИС и ГОСТ-сертификата. Поэтому изменения проверяются офлайн и против реального ЕСИА могут быть не протестированы. Если обновление заработало (или нет) с реальной средой, пожалуйста, оставьте короткий отчёт в обсуждении: Отчёты о совместимости с реальной средой ЕСИА.
Основная цель библиотеки — получение токена ЕСИА. Получив токен, вы можете выполнять запросы к API; библиотека предоставляет самые базовые методы, а не все существующие в API ЕСИА.
Оглавление
- Требования
- Установка
- Быстрый старт
- Организации и роли
- OAuth state (защита от CSRF)
- Конфигурация
- Проверка JWT (подпись и claim’ы)
- Подпись запросов (сигнеры)
- Работа с токеном и oid
- Обратная совместимость и обновление
- Тестирование
- Лицензия
Требования
PHP >= 8.3, расширения ext-openssl и ext-json.
Библиотека не привязана к конкретному HTTP-клиенту и использует стандарты PSR-18 (HTTP Client) и PSR-17 (HTTP Factories). Необходимо установить любую их реализацию, например Guzzle:
composer require guzzlehttp/guzzle
Подойдёт любой PSR-18 клиент (Guzzle, Symfony HTTP Client, и т.д.) вместе с
PSR-17 фабрикой (nyholm/psr7, guzzlehttp/psr7 и т.п.). Реализация
находится автоматически (через php-http/discovery), либо её можно передать
явно в конструктор OpenId:
$esia = new \Esia\OpenId($config, $psr18Client, $psr17RequestFactory, $psr17StreamFactory);
Установка
При помощи composer:
composer require --prefer-dist fr05t1k/esia
Или добавьте в composer.json
"fr05t1k/esia" : "^3.0"
Быстрый старт
Пример получения ссылки для авторизации
<?php
$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="<?=$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();
// Организации и роли пользователя (нужен полученный токен):
$roles = $esia->getRoles(); // членство в организациях + роли (одним запросом)
$organizations = $esia->getOrganizations(); // подробные данные по каждой организации
Организации и роли
Метод getRoles() возвращает список организаций, в которых состоит пользователь,
вместе с ролями (эндпоинт rs/prns/{oid}/roles). Данные приходят одним запросом —
каждый элемент содержит oid организации, краткое/полное наименование, ОГРН,
флаги chief/admin и т.д. Если пользователь не состоит ни в одной организации,
возвращается пустой массив.
Метод getOrganizations() использует коллекцию rs/prns/{oid}/orgs, элементы
которой — ссылки на ресурсы организаций; каждая ссылка догружается и возвращается
полными данными организации.
// Токен уже получен через getToken()
$roles = $esia->getRoles();
foreach ($roles as $role) {
echo $role['shortName'], PHP_EOL;
}
$organizations = $esia->getOrganizations();
OAuth state (защита от CSRF)
state — одноразовый идентификатор запроса авторизации. Библиотека генерирует
его автоматически при вызове buildUrl()/getToken() и сохраняет в Config,
поэтому его можно получить и сверить при возврате пользователя:
$config = new \Esia\Config([/* ... */]);
$esia = new \Esia\OpenId($config);
$url = $esia->buildUrl();
$state = $config->getState(); // сохраните в сессии
// ... после редиректа сверьте $_GET['state'] с сохранённым значением
Свой state можно передать и напрямую в buildUrl($state) — удобно, когда
нужно различать несколько одновременно открытых окон входа. Переданное значение
попадёт в URL и сохранится в Config:
$url = $esia->buildUrl($myState);
Можно задать свой state — через параметр конфигурации или setState() — тогда
он будет использован вместо сгенерированного:
$config = new \Esia\Config([/* ... */, 'state' => $myState]);
// или
$config->setState($myState);
Конфигурация
| Параметр | По умолчанию | Описание |
|---|---|---|
clientId |
— | ID вашего приложения. |
redirectUrl |
— | URL, куда будет перенаправлен ответ с кодом. |
portalUrl |
https://esia-portal1.test.gosuslugi.ru/ |
Домен портала для авторизации (только домен), по умолчанию — тестовая среда. Для продуктивной среды укажите https://esia.gosuslugi.ru/. |
codeUrlPath |
aas/oauth2/v2/ac |
URL для получения кода авторизации. Прежний путь aas/oauth2/ac выведен из эксплуатации; при необходимости старое значение можно вернуть через этот параметр. |
tokenUrlPath |
aas/oauth2/v3/te |
URL для получения токена. Прежний путь aas/oauth2/te выведен из эксплуатации; при необходимости старое значение можно вернуть через этот параметр. |
clientCertificateHash |
пусто | Хэш (fingerprint) сертификата системы-клиента в hex-формате. Обязателен для актуального эндпоинта получения токена aas/oauth2/v3/te (иначе ЕСИА вернёт ESIA-007014). Сертификат должен быть предварительно зарегистрирован в ЕСИА и привязан к УЗ системы-клиента. Значение — отпечаток сертификата по SHA-256 в hex (например, openssl x509 -in cert.pem -noout -fingerprint -sha256); точный способ вычисления см. в методических рекомендациях ЕСИА. Если не задан, не отправляется (обратная совместимость). |
scope |
fullname birthdate gender email mobile id_doc snils inn |
Запрашиваемые права у пользователя. |
privateKeyPath |
— | Путь до приватного ключа. |
privateKeyPassword |
— | Пароль от приватного ключа. |
certPath |
— | Путь до сертификата. |
tmpPath |
— | Путь до директории, где будет проходить подпись (должна быть доступна для записи). |
state |
пусто | OAuth-идентификатор запроса (анти-CSRF). Если не задан, генерируется автоматически при buildUrl()/getToken() и сохраняется в Config (доступен через getState()). Можно задать свой — он будет использован вместо сгенерированного. |
esiaCertPath |
пусто | Путь до сертификата подписи ЕСИА (для продуктивной среды — GOST-2012). Если задан, полученный JWT автоматически проверяется: подпись, exp/nbf/iat, iss и аудитория (aud/client_id). Если не задан, проверка пропускается (обратная совместимость). |
esiaTokenIssuer |
пусто | Ожидаемое значение claim iss в токене. Если не задано, iss не проверяется. |
tokenLeeway |
60 |
Допустимое отклонение (в секундах) при проверке временных claim’ов (exp, nbf, iat) для компенсации рассинхронизации часов. |
Проверка JWT (подпись и claim’ы)
По умолчанию библиотека не проверяет подпись полученного от ЕСИА токена — это
сохраняет обратную совместимость. Чтобы включить проверку, укажите путь до
сертификата подписи ЕСИА через esiaCertPath (и, при необходимости,
esiaTokenIssuer):
$config = new \Esia\Config([
// ... остальные параметры
'esiaCertPath' => '/path/to/esia-signing-cert.pem',
'esiaTokenIssuer' => 'http://esia.gosuslugi.ru/',
]);
$esia = new \Esia\OpenId($config);
// getToken выбросит наследника InvalidTokenException при некорректном токене:
// SignatureInvalidException — неверная подпись,
// TokenExpiredException — истёк срок (exp) или ещё не действителен (nbf),
// InvalidClaimException — неверный iss / аудитория (aud/client_id).
$token = $esia->getToken($_GET['code']);
Проверка сделана подключаемой (pluggable): вы можете передать собственную
реализацию \Esia\Token\TokenValidatorInterface (например, с проверкой
GOST-подписи через CryptoPro) через \Esia\OpenId::setTokenValidator().
Стандартная реализация \Esia\Token\JwtValidator проверяет подпись через
\Esia\Token\OpenSslSignatureVerifier (RSA из коробки; алгоритмы GOST-2012 —
при наличии GOST-движка в OpenSSL).
Подпись запросов (сигнеры)
Для получения токена запрос к ЕСИА должен быть подписан. ЕСИА требует подпись
ГОСТ Р 34.10-2012 (RSA больше не поддерживается в продуктивной среде).
Сигнер задаётся через \Esia\OpenId::setSigner() и должен реализовывать
\Esia\Signer\SignerInterface. Доступны следующие реализации:
| Сигнер | Механизм | Требования |
|---|---|---|
\Esia\Signer\SignerPKCS7 |
Нативный openssl_pkcs7_sign() |
Стандартный PHP с ext-openssl. Не умеет ГОСТ в обычной сборке OpenSSL — подходит только для тестов/RSA. |
\Esia\Signer\CliSignerPKCS7 |
Вызов openssl smime -engine gost |
OpenSSL, собранный с GOST-движком (libengine-gost-openssl1.1), в PATH. Ключ и сертификат ГОСТ в PEM. |
\Esia\Signer\CliCryptoProSigner |
Вызов утилиты csptest |
Установленный КриптоПро CSP с утилитой csptest. Ключ ГОСТ в контейнере CSP, указывается по имени контейнера. |
\Esia\Signer\CryptoProSigner |
PHP-расширение КриптоПро (\CPRawSignature) |
Проприетарное PHP-расширение КриптоПро. Сертификат ГОСТ в хранилище My текущего пользователя. |
Пример с CLI-сигнером ГОСТ (OpenSSL + GOST-движок):
$esia->setSigner(new \Esia\Signer\CliSignerPKCS7(
'/path/to/gost-cert.pem',
'/path/to/gost-key.pem',
'key-password',
'/tmp'
));
Пример с КриптоПро через утилиту csptest (подпись формируется по
методическим рекомендациям ЕСИА: «сырое» значение подписи разворачивается
побайтово и кодируется в base64 url-safe):
$esia->setSigner(new \Esia\Signer\CliCryptoProSigner(
'my-container', // имя контейнера ключа в CSP
'password', // пароль контейнера (если задан)
'csptest', // путь до csptest (по умолчанию 'csptest')
'/tmp' // каталог для временных файлов (по умолчанию системный)
));
Пример с КриптоПро через PHP-расширение:
$esia->setSigner(new \Esia\Signer\CryptoProSigner(
'745187e5c161cd2e3130d886f9df4492fa270685', // отпечаток сертификата
'pin' // PIN контейнера (если задан)
));
Все сигнеры поддерживают PSR-3 логгер через setLogger().
Сигнер и логгер также можно передать сразу в конструктор \Esia\OpenId
(удобно для DI-контейнеров), не прибегая к сеттерам:
$esia = new \Esia\OpenId(
$config,
$httpClient, // ?Psr\Http\Client\ClientInterface
$requestFactory, // ?Psr\Http\Message\RequestFactoryInterface
$streamFactory, // ?Psr\Http\Message\StreamFactoryInterface
$signer, // ?Esia\Signer\SignerInterface
$logger // ?Psr\Log\LoggerInterface
);
Любой из аргументов можно передать как null — тогда используется значение по
умолчанию. Методы setSigner() и setLogger() продолжают работать.
Работа с токеном и oid
Токен — JWT-токен, который вы получаете от ЕСИА для дальнейшего взаимодействия.
oid — уникальный идентификатор владельца токена.
Как получить oid?
Есть 2 способа:
- oid содержится в JWT-токене — расшифровав его;
- после получения токена oid сохраняется в
Configи доступен так:$esia->getConfig()->getOid();
Переиспользование токена
Дополнительно укажите токен и идентификатор в конфиге:
$config->setToken($jwt);
$config->setOid($oid);
Обратная совместимость и обновление
Начиная с версии 3.0 значения по умолчанию обновлены под актуальный API ЕСИА
(Методические рекомендации): эндпоинты aas/oauth2/v2/ac и aas/oauth2/v3/te,
portalUrl использует https://. Актуальный aas/oauth2/v3/te требует параметр
clientCertificateHash. Если вы полагались на прежние значения по умолчанию
(aas/oauth2/ac, aas/oauth2/te, http://), задайте их явно через Config.
Обновляетесь с 2.x? См. подробное руководство по обновлению 2.4.2 → 3.0.0.
Тестирование
Основное покрытие в библиотеке — офлайн, без обращения к ЕСИА (HTTP-моки,
JWT-фикстуры, локальный ГОСТ round-trip и end-to-end против локального
мок-сервера ЕСИА). Живой тестовый стенд esia-portal1.test.gosuslugi.ru
доступен только из РФ и требует зарегистрированной ИС и ГОСТ-сертификата.
Быстрый запуск:
composer install
vendor/bin/codecept build # генерирует акторов для наборов unit и e2e
vendor/bin/codecept run # все наборы
vendor/bin/codecept run e2e # только end-to-end против мок-сервера
Подробнее — как устроено офлайн-тестирование, как запускать ГОСТ-тесты и на что
библиотека настроена для реального стенда ЕСИА по умолчанию — в руководстве по
тестированию (docs/testing.md).
Лицензия
Распространяется под лицензией MIT.