Repository: fr05t1k/esia Branch: master Commit: c1750575b7eb Files: 42 Total size: 73.0 KB Directory structure: gitextract_ro8_8jf5/ ├── .gitignore ├── .travis.yml ├── README.md ├── _config.yml ├── codeception.yml ├── composer.json ├── src/ │ └── Esia/ │ ├── Config.php │ ├── Exceptions/ │ │ ├── AbstractEsiaException.php │ │ ├── ForbiddenException.php │ │ ├── InvalidConfigurationException.php │ │ └── RequestFailException.php │ ├── Http/ │ │ ├── Exceptions/ │ │ │ └── HttpException.php │ │ └── GuzzleHttpClient.php │ ├── OpenId.php │ └── Signer/ │ ├── AbstractSignerPKCS7.php │ ├── CliSignerPKCS7.php │ ├── Exceptions/ │ │ ├── CannotGenerateRandomIntException.php │ │ ├── CannotReadCertificateException.php │ │ ├── CannotReadPrivateKeyException.php │ │ ├── NoSuchCertificateFileException.php │ │ ├── NoSuchKeyFileException.php │ │ ├── NoSuchTmpDirException.php │ │ └── SignFailException.php │ ├── SignerInterface.php │ └── SignerPKCS7.php └── tests/ ├── .configure-gost-openssl.sh ├── _bootstrap.php ├── _data/ │ ├── server-gost.crt │ ├── server-gost.key │ ├── server.crt │ ├── server.csr │ └── server.key ├── _support/ │ ├── Helper/ │ │ └── Unit.php │ ├── UnitTester.php │ └── _generated/ │ └── UnitTesterActions.php ├── unit/ │ ├── ConfigTest.php │ ├── Http/ │ │ └── GuzzleHttpClientTest.php │ ├── OpenIdCliOpensslTest.php │ ├── OpenIdTest.php │ ├── Signer/ │ │ └── SignerPKCS7Test.php │ └── _bootstrap.php └── unit.suite.yml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .idea tests/tmp/* tests/_output/* public/* vendor tests/_data/non_readable_file ================================================ FILE: .travis.yml ================================================ dist: bionic language: php addons: apt: packages: - libengine-gost-openssl1.1 before_install: - sudo bash tests/.configure-gost-openssl.sh php: - 7.1 - 7.2 - 7.3 - 7.4 - 8.0 install: - travis_retry composer self-update - travis_retry composer --version - travis_retry composer update --prefer-dist --no-interaction script: - chmod 000 tests/_data/non_readable_file - php vendor/codeception/codeception/codecept run ================================================ FILE: README.md ================================================ # Единая система идентификации и аутентификации (ЕСИА) OpenId [![Build Status](https://travis-ci.org/fr05t1k/esia.svg?branch=master)](https://travis-ci.org/fr05t1k/esia) # Описание Компонент для авторизации на портале "Госуслуги". # Внимание! Получив токен вы можете выполнять любые API запросы. Библиотека не поддерживает все существующие методы в API, а предоставляет только самые базовые. Основная цель библиотеки - получение токена. # Установка При помощи [composer](https://getcomposer.org/download/): ``` composer require --prefer-dist fr05t1k/esia ``` Или добавьте в composer.json ``` "fr05t1k/esia" : "^2.0" ``` # Как использовать Пример получения ссылки для авторизации ```php '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' )); ?> Войти через портал госуслуги ``` После редиректа на ваш `redirectUrl` вы получите в `$_GET['code']` код для получения токена Пример получения токена и информации о пользователе ```php $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` - путь до дериктории где будет проходить подпись (должна быть доступна для записи). # Токен и oid Токен - jwt токен которые вы получаете от ЕСИА для дальнейшего взаимодействия oid - уникальный идентификатор владельца токена ## Как получить oid? Если 2 способа: 1. oid содержится в jwt токене, расшифровав его 2. После получения токена oid сохраняется в config и получить можно так ```php $esia->getConfig()->getOid(); ``` ## Переиспользование Токена Дополнительно укажите токен и идентификатор в конфиге ```php $config->setToken($jwt); $config->setOid($oid); ``` ================================================ FILE: _config.yml ================================================ theme: jekyll-theme-cayman ================================================ FILE: codeception.yml ================================================ actor: Tester paths: tests: tests log: tests/_output data: tests/_data support: tests/_support envs: tests/_envs bootstrap: _bootstrap.php settings: colors: true memory_limit: 1024M extensions: enabled: - Codeception\Extension\RunFailed coverage: enabled: true remote: false whitelist: include: - src/* ================================================ FILE: composer.json ================================================ { "name": "fr05t1k/esia", "license": "MIT", "description": "OpenID ESIA authenticating", "keywords": [ "esia", "openid", "egov" ], "autoload": { "psr-4": { "Esia\\": "src/Esia" } }, "autoload-dev": { "psr-4": { "tests\\" : "tests" } }, "require": { "php": "^7.1|^8.0", "guzzlehttp/guzzle": "^6.1.0|^7.0", "psr/log": "^1.0", "psr/http-message": "^1.0", "psr/http-client": "^1.0" }, "suggest": { "ext-openssl": "SignerPKCS7 support" }, "require-dev": { "roave/security-advisories": "dev-latest", "codeception/codeception": "^4.0", "codeception/module-asserts": "^1.3" } } ================================================ FILE: src/Esia/Config.php ================================================ clientId = $config['clientId'] ?? $this->clientId; if (!$this->clientId) { throw new InvalidConfigurationException('Please provide clientId'); } $this->redirectUrl = $config['redirectUrl'] ?? $this->redirectUrl; if (!$this->redirectUrl) { throw new InvalidConfigurationException('Please provide redirectUrl'); } $this->privateKeyPath = $config['privateKeyPath'] ?? $this->privateKeyPath; if (!$this->privateKeyPath) { throw new InvalidConfigurationException('Please provide privateKeyPath'); } $this->certPath = $config['certPath'] ?? $this->certPath; if (!$this->certPath) { throw new InvalidConfigurationException('Please provide certPath'); } $this->portalUrl = $config['portalUrl'] ?? $this->portalUrl; $this->tokenUrlPath = $config['tokenUrlPath'] ?? $this->tokenUrlPath; $this->codeUrlPath = $config['codeUrlPath'] ?? $this->codeUrlPath; $this->personUrlPath = $config['personUrlPath'] ?? $this->personUrlPath; $this->logoutUrlPath = $config['logoutUrlPath'] ?? $this->logoutUrlPath; $this->privateKeyPassword = $config['privateKeyPassword'] ?? $this->privateKeyPassword; $this->oid = $config['oid'] ?? $this->oid; $this->scope = $config['scope'] ?? $this->scope; if (!is_array($this->scope)) { throw new InvalidConfigurationException('scope must be array of strings'); } $this->responseType = $config['responseType'] ?? $this->responseType; $this->accessType = $config['accessType'] ?? $this->accessType; $this->tmpPath = $config['tmpPath'] ?? $this->tmpPath; $this->token = $config['token'] ?? $this->token; } public function getPortalUrl(): string { return $this->portalUrl; } public function getPrivateKeyPath(): string { return $this->privateKeyPath; } public function getPrivateKeyPassword(): string { return $this->privateKeyPassword; } public function getCertPath(): string { return $this->certPath; } public function getOid(): string { return $this->oid; } public function setOid(string $oid): void { $this->oid = $oid; } public function getScope(): array { return $this->scope; } public function getScopeString(): string { return implode(' ', $this->scope); } public function getResponseType(): string { return $this->responseType; } public function getAccessType(): string { return $this->accessType; } public function getTmpPath(): string { return $this->tmpPath; } public function getToken(): ?string { return $this->token; } public function setToken(string $token): void { $this->token = $token; } public function getClientId(): string { return $this->clientId; } public function getRedirectUrl(): string { return $this->redirectUrl; } /** * Return an url for request to get an access token */ public function getTokenUrl(): string { return $this->portalUrl . $this->tokenUrlPath; } /** * Return an url for request to get an authorization code */ public function getCodeUrl(): string { return $this->portalUrl . $this->codeUrlPath; } /** * @return string * @throws InvalidConfigurationException */ public function getPersonUrl(): string { if (!$this->oid) { throw new InvalidConfigurationException('Please provide oid'); } return $this->portalUrl . $this->personUrlPath . '/' . $this->oid; } /** * Return an url for logout */ public function getLogoutUrl(): string { return $this->portalUrl . $this->logoutUrlPath; } } ================================================ FILE: src/Esia/Exceptions/AbstractEsiaException.php ================================================ guzzle = $guzzle; } /** * Sends a PSR-7 request and returns a PSR-7 response. * * Every technically correct HTTP response MUST be returned as is, even if it represents a HTTP * error response or a redirect instruction. The only special case is 1xx responses, which MUST * be assembled in the HTTP client. * * The client MAY do modifications to the Request before sending it. Because PSR-7 objects are * immutable, one cannot assume that the object passed to ClientInterface::sendRequest() will be the same * object that is actually sent. For example the Request object that is returned by an exception MAY * be a different object than the one passed to sendRequest, so comparison by reference (===) is not possible. * * {@link https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message-meta.md#why-value-objects} * * @param RequestInterface $request * * @return ResponseInterface * * @throws ClientExceptionInterface If an error happens during processing the request. */ public function sendRequest(RequestInterface $request): ResponseInterface { try { return $this->guzzle->send($request); } catch (GuzzleException $e) { throw new HttpException($e->getMessage(), $e->getCode(), $e); } } } ================================================ FILE: src/Esia/OpenId.php ================================================ config = $config; $this->client = $client ?? new GuzzleHttpClient(new Client()); $this->logger = new NullLogger(); $this->signer = new SignerPKCS7( $config->getCertPath(), $config->getPrivateKeyPath(), $config->getPrivateKeyPassword(), $config->getTmpPath() ); } /** * Replace default signer */ public function setSigner(SignerInterface $signer): void { $this->signer = $signer; } /** * Get config */ public function getConfig(): Config { return $this->config; } /** * Return an url for authentication * * ```php * Login * ``` * * @return string|false * @throws SignFailException */ public function buildUrl() { $timestamp = $this->getTimeStamp(); $state = $this->buildState(); $message = $this->config->getScopeString() . $timestamp . $this->config->getClientId() . $state; $clientSecret = $this->signer->sign($message); $url = $this->config->getCodeUrl() . '?%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, ]; $request = http_build_query($params); return sprintf($url, $request); } /** * Return an url for logout */ public function buildLogoutUrl(string $redirectUrl = null): string { $url = $this->config->getLogoutUrl() . '?%s'; $params = [ 'client_id' => $this->config->getClientId(), ]; if ($redirectUrl) { $params['redirect_url'] = $redirectUrl; } $request = http_build_query($params); return sprintf($url, $request); } /** * Method collect a token with given code * * @throws SignFailException * @throws AbstractEsiaException */ public function getToken(string $code): string { $timestamp = $this->getTimeStamp(); $state = $this->buildState(); $clientSecret = $this->signer->sign( $this->config->getScopeString() . $timestamp . $this->config->getClientId() . $state ); $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, ]; $payload = $this->sendRequest( new Request( 'POST', $this->config->getTokenUrl(), [ 'Content-Type' => 'application/x-www-form-urlencoded', ], http_build_query($body) ) ); $this->logger->debug('Payload: ', $payload); $token = $payload['access_token']; $this->config->setToken($token); # get object id from token $chunks = explode('.', $token); $payload = json_decode($this->base64UrlSafeDecode($chunks[1]), true); $this->config->setOid($payload['urn:esia:sbj_id']); return $token; } /** * Fetch person info from current person * * You must collect token person before * calling this method * * @throws AbstractEsiaException */ public function getPersonInfo(): array { $url = $this->config->getPersonUrl(); return $this->sendRequest(new Request('GET', $url)); } /** * Fetch contact info about current person * * You must collect token person before * calling this method * * @throws Exceptions\InvalidConfigurationException * @throws AbstractEsiaException */ public function getContactInfo(): array { $url = $this->config->getPersonUrl() . '/ctts'; $payload = $this->sendRequest(new Request('GET', $url)); if ($payload && $payload['size'] > 0) { return $this->collectArrayElements($payload['elements']); } return $payload; } /** * Fetch address from current person * * You must collect token person before * calling this method * * @throws Exceptions\InvalidConfigurationException * @throws AbstractEsiaException */ public function getAddressInfo(): array { $url = $this->config->getPersonUrl() . '/addrs'; $payload = $this->sendRequest(new Request('GET', $url)); if ($payload['size'] > 0) { return $this->collectArrayElements($payload['elements']); } return $payload; } /** * Fetch documents info about current person * * You must collect token person before * calling this method * * @throws Exceptions\InvalidConfigurationException * @throws AbstractEsiaException */ public function getDocInfo(): array { $url = $this->config->getPersonUrl() . '/docs'; $payload = $this->sendRequest(new Request('GET', $url)); if ($payload && $payload['size'] > 0) { return $this->collectArrayElements($payload['elements']); } return $payload; } /** * This method can iterate on each element * and fetch entities from esia by url * * @throws AbstractEsiaException */ private function collectArrayElements($elements): array { $result = []; foreach ($elements as $elementUrl) { $elementPayload = $this->sendRequest(new Request('GET', $elementUrl)); if ($elementPayload) { $result[] = $elementPayload; } } return $result; } /** * @throws AbstractEsiaException */ private function sendRequest(RequestInterface $request): array { try { if ($this->config->getToken()) { /** @noinspection CallableParameterUseCaseInTypeContextInspection */ $request = $request->withHeader('Authorization', 'Bearer ' . $this->config->getToken()); } $response = $this->client->sendRequest($request); $responseBody = json_decode($response->getBody()->getContents(), true); if (!is_array($responseBody)) { throw new RuntimeException( sprintf( 'Cannot decode response body. JSON error (%d): %s', json_last_error(), json_last_error_msg() ) ); } return $responseBody; } catch (ClientExceptionInterface $e) { $this->logger->error('Request was failed', ['exception' => $e]); $prev = $e->getPrevious(); // Only for Guzzle if ($prev instanceof BadResponseException && $prev->getResponse() !== null && $prev->getResponse()->getStatusCode() === 403 ) { throw new ForbiddenException('Request is forbidden', 0, $e); } throw new RequestFailException('Request is failed', 0, $e); } catch (RuntimeException $e) { $this->logger->error('Cannot read body', ['exception' => $e]); throw new RequestFailException('Cannot read body', 0, $e); } catch (InvalidArgumentException $e) { $this->logger->error('Wrong header', ['exception' => $e]); throw new RequestFailException('Wrong header', 0, $e); } } private function getTimeStamp(): string { return date('Y.m.d H:i:s O'); } /** * Generate state with uuid * * @throws SignFailException */ private function buildState(): string { try { return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', random_int(0, 0xffff), random_int(0, 0xffff), random_int(0, 0xffff), random_int(0, 0x0fff) | 0x4000, random_int(0, 0x3fff) | 0x8000, random_int(0, 0xffff), random_int(0, 0xffff), random_int(0, 0xffff) ); } catch (Exception $e) { throw new CannotGenerateRandomIntException('Cannot generate random integer', $e); } } /** * Url safe for base64 */ private function base64UrlSafeDecode(string $string): string { $base64 = strtr($string, '-_', '+/'); return base64_decode($base64); } } ================================================ FILE: src/Esia/Signer/AbstractSignerPKCS7.php ================================================ certPath = $certPath; $this->privateKeyPath = $privateKeyPath; $this->privateKeyPassword = $privateKeyPassword; $this->tmpPath = $tmpPath; $this->logger = new NullLogger(); } /** * Temporary directory for message signing (must me writable) * * @var string */ protected $tmpPath; /** * @throws SignFailException */ protected function checkFilesExists(): void { if (!file_exists($this->certPath)) { throw new NoSuchCertificateFileException('Certificate does not exist'); } if (!is_readable($this->certPath)) { throw new CannotReadCertificateException('Cannot read the certificate'); } if (!file_exists($this->privateKeyPath)) { throw new NoSuchKeyFileException('Private key does not exist'); } if (!is_readable($this->privateKeyPath)) { throw new CannotReadPrivateKeyException('Cannot read the private key'); } if (!file_exists($this->tmpPath)) { throw new NoSuchTmpDirException('Temporary folder is not found'); } if (!is_writable($this->tmpPath)) { throw new NoSuchTmpDirException('Temporary folder is not writable'); } } /** * Generate random unique string */ protected function getRandomString(): string { return md5(uniqid(mt_rand(), true)); } /** * Url safe for base64 */ protected function urlSafe(string $string): string { return rtrim(strtr(trim($string), '+/', '-_'), '='); } } ================================================ FILE: src/Esia/Signer/CliSignerPKCS7.php ================================================ checkFilesExists(); // random unique directories for sign $messageFile = $this->tmpPath . DIRECTORY_SEPARATOR . $this->getRandomString(); $signFile = $this->tmpPath . DIRECTORY_SEPARATOR . $this->getRandomString(); file_put_contents($messageFile, $message); $this->run( 'openssl ' . 'smime -engine gost -sign -binary -outform DER -noattr ' . '-signer ' . escapeshellarg($this->certPath) . ' ' . '-inkey ' . escapeshellarg($this->privateKeyPath) . ' ' . '-passin ' . escapeshellarg('pass:' . $this->privateKeyPassword) . ' ' . '-in ' . escapeshellarg($messageFile) . ' ' . '-out ' . escapeshellarg($signFile) ); $signed = file_get_contents($signFile); if ($signed === false) { $message = sprintf('cannot read %s file', $signFile); $this->logger->error($message); throw new SignFailException($message); } $sign = $this->urlSafe(base64_encode($signed)); unlink($signFile); unlink($messageFile); return $sign; } /** * @throws SignFailException */ private function run(string $command): void { $process = proc_open( $command, [ ['pipe', 'w'], // stdout ['pipe', 'w'], // stderr ], $pipes ); $result = stream_get_contents($pipes[0]); fclose($pipes[0]); $errors = stream_get_contents($pipes[1]); fclose($pipes[1]); $code = proc_close($process); if (0 !== $code || $result === false) { $errors = $errors ?: 'unknown'; $this->logger->error('Sign fail'); $this->logger->error('SSL error: ' . $errors); throw new SignFailException($errors); } } } ================================================ FILE: src/Esia/Signer/Exceptions/CannotGenerateRandomIntException.php ================================================ pkcs7Flags |= $pkcs7Flag; } /** * @throws SignFailException */ public function sign(string $message): string { $this->checkFilesExists(); $certContent = file_get_contents($this->certPath); $keyContent = file_get_contents($this->privateKeyPath); $cert = openssl_x509_read($certContent); if ($cert === false) { throw new CannotReadCertificateException('Cannot read the certificate: ' . openssl_error_string()); } $this->logger->debug('Cert: ' . print_r($cert, true), ['cert' => $cert]); $privateKey = openssl_pkey_get_private($keyContent, $this->privateKeyPassword); if ($privateKey === false) { throw new CannotReadPrivateKeyException('Cannot read the private key: ' . openssl_error_string()); } $this->logger->debug('Private key: : ' . print_r($privateKey, true), ['privateKey' => $privateKey]); // random unique directories for sign $messageFile = $this->tmpPath . DIRECTORY_SEPARATOR . $this->getRandomString(); $signFile = $this->tmpPath . DIRECTORY_SEPARATOR . $this->getRandomString(); file_put_contents($messageFile, $message); $signResult = openssl_pkcs7_sign( $messageFile, $signFile, $cert, $privateKey, [], $this->pkcs7Flags ); if ($signResult) { $this->logger->debug('Sign success'); } else { $this->logger->error('Sign fail'); $this->logger->error('SSL error: ' . openssl_error_string()); throw new SignFailException('Cannot sign the message'); } $signed = file_get_contents($signFile); # split by section $signed = explode("\n\n", $signed); # get third section which contains sign and join into one line $sign = str_replace("\n", '', $this->urlSafe($signed[3])); unlink($signFile); unlink($messageFile); return $sign; } } ================================================ FILE: tests/.configure-gost-openssl.sh ================================================ sed -i '1iopenssl_conf=openssl_def' /usr/lib/ssl/openssl.cnf tee -a /usr/lib/ssl/openssl.cnf <assertEquals(5, $element->getChildrenCount()); * ``` * * Floating-point example: * ```php * assertEquals(0.3, $calculator->add(0.1, 0.2), 'Calculator should add the two numbers correctly.', 0.01); * ``` * * @param $expected * @param $actual * @param string $message * @param float $delta * @see \Codeception\Module\Asserts::assertEquals() */ public function assertEquals($expected, $actual, $message = null, $delta = null) { return $this->getScenario()->runStep(new Action('assertEquals', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * Checks that two variables are not equal. If you're comparing floating-point values, * you can specify the optional "delta" parameter which dictates how great of a precision * error are you willing to tolerate in order to consider the two values not equal. * * Regular example: * ```php * assertNotEquals(0, $element->getChildrenCount()); * ``` * * Floating-point example: * ```php * assertNotEquals(0.4, $calculator->add(0.1, 0.2), 'Calculator should add the two numbers correctly.', 0.01); * ``` * * @param $expected * @param $actual * @param string $message * @param float $delta * @see \Codeception\Module\Asserts::assertNotEquals() */ public function assertNotEquals($expected, $actual, $message = null, $delta = null) { return $this->getScenario()->runStep(new Action('assertNotEquals', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * Checks that two variables are same * * @param $expected * @param $actual * @param string $message * @see \Codeception\Module\Asserts::assertSame() */ public function assertSame($expected, $actual, $message = null) { return $this->getScenario()->runStep(new Action('assertSame', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * Checks that two variables are not same * * @param $expected * @param $actual * @param string $message * @see \Codeception\Module\Asserts::assertNotSame() */ public function assertNotSame($expected, $actual, $message = null) { return $this->getScenario()->runStep(new Action('assertNotSame', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * Checks that actual is greater than expected * * @param $expected * @param $actual * @param string $message * @see \Codeception\Module\Asserts::assertGreaterThan() */ public function assertGreaterThan($expected, $actual, $message = null) { return $this->getScenario()->runStep(new Action('assertGreaterThan', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * Checks that actual is greater or equal than expected * * @param $expected * @param $actual * @param string $message * @see \Codeception\Module\Asserts::assertGreaterThanOrEqual() */ public function assertGreaterThanOrEqual($expected, $actual, $message = null) { return $this->getScenario()->runStep(new Action('assertGreaterThanOrEqual', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * Checks that actual is less than expected * * @param $expected * @param $actual * @param string $message * @see \Codeception\Module\Asserts::assertLessThan() */ public function assertLessThan($expected, $actual, $message = null) { return $this->getScenario()->runStep(new Action('assertLessThan', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * Checks that actual is less or equal than expected * * @param $expected * @param $actual * @param string $message * @see \Codeception\Module\Asserts::assertLessThanOrEqual() */ public function assertLessThanOrEqual($expected, $actual, $message = null) { return $this->getScenario()->runStep(new Action('assertLessThanOrEqual', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * Checks that haystack contains needle * * @param $needle * @param $haystack * @param string $message * @see \Codeception\Module\Asserts::assertContains() */ public function assertContains($needle, $haystack, $message = null) { return $this->getScenario()->runStep(new Action('assertContains', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * Checks that haystack doesn't contain needle. * * @param $needle * @param $haystack * @param string $message * @see \Codeception\Module\Asserts::assertNotContains() */ public function assertNotContains($needle, $haystack, $message = null) { return $this->getScenario()->runStep(new Action('assertNotContains', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * Checks that string match with pattern * * @param string $pattern * @param string $string * @param string $message * @see \Codeception\Module\Asserts::assertRegExp() */ public function assertRegExp($pattern, $string, $message = null) { return $this->getScenario()->runStep(new Action('assertRegExp', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * Checks that string not match with pattern * * @param string $pattern * @param string $string * @param string $message * @see \Codeception\Module\Asserts::assertNotRegExp() */ public function assertNotRegExp($pattern, $string, $message = null) { return $this->getScenario()->runStep(new Action('assertNotRegExp', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * Checks that a string starts with the given prefix. * * @param string $prefix * @param string $string * @param string $message * @see \Codeception\Module\Asserts::assertStringStartsWith() */ public function assertStringStartsWith($prefix, $string, $message = null) { return $this->getScenario()->runStep(new Action('assertStringStartsWith', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * Checks that a string doesn't start with the given prefix. * * @param string $prefix * @param string $string * @param string $message * @see \Codeception\Module\Asserts::assertStringStartsNotWith() */ public function assertStringStartsNotWith($prefix, $string, $message = null) { return $this->getScenario()->runStep(new Action('assertStringStartsNotWith', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * Checks that variable is empty. * * @param $actual * @param string $message * @see \Codeception\Module\Asserts::assertEmpty() */ public function assertEmpty($actual, $message = null) { return $this->getScenario()->runStep(new Action('assertEmpty', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * Checks that variable is not empty. * * @param $actual * @param string $message * @see \Codeception\Module\Asserts::assertNotEmpty() */ public function assertNotEmpty($actual, $message = null) { return $this->getScenario()->runStep(new Action('assertNotEmpty', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * Checks that variable is NULL * * @param $actual * @param string $message * @see \Codeception\Module\Asserts::assertNull() */ public function assertNull($actual, $message = null) { return $this->getScenario()->runStep(new Action('assertNull', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * Checks that variable is not NULL * * @param $actual * @param string $message * @see \Codeception\Module\Asserts::assertNotNull() */ public function assertNotNull($actual, $message = null) { return $this->getScenario()->runStep(new Action('assertNotNull', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * Checks that condition is positive. * * @param $condition * @param string $message * @see \Codeception\Module\Asserts::assertTrue() */ public function assertTrue($condition, $message = null) { return $this->getScenario()->runStep(new Action('assertTrue', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * Checks that the condition is NOT true (everything but true) * * @param $condition * @param string $message * @see \Codeception\Module\Asserts::assertNotTrue() */ public function assertNotTrue($condition, $message = null) { return $this->getScenario()->runStep(new Action('assertNotTrue', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * Checks that condition is negative. * * @param $condition * @param string $message * @see \Codeception\Module\Asserts::assertFalse() */ public function assertFalse($condition, $message = null) { return $this->getScenario()->runStep(new Action('assertFalse', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * Checks that the condition is NOT false (everything but false) * * @param $condition * @param string $message * @see \Codeception\Module\Asserts::assertNotFalse() */ public function assertNotFalse($condition, $message = null) { return $this->getScenario()->runStep(new Action('assertNotFalse', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * Checks if file exists * * @param string $filename * @param string $message * @see \Codeception\Module\Asserts::assertFileExists() */ public function assertFileExists($filename, $message = null) { return $this->getScenario()->runStep(new Action('assertFileExists', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * Checks if file doesn't exist * * @param string $filename * @param string $message * @see \Codeception\Module\Asserts::assertFileNotExists() */ public function assertFileNotExists($filename, $message = null) { return $this->getScenario()->runStep(new Action('assertFileNotExists', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * @param $expected * @param $actual * @param $description * @see \Codeception\Module\Asserts::assertGreaterOrEquals() */ public function assertGreaterOrEquals($expected, $actual, $description = null) { return $this->getScenario()->runStep(new Action('assertGreaterOrEquals', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * @param $expected * @param $actual * @param $description * @see \Codeception\Module\Asserts::assertLessOrEquals() */ public function assertLessOrEquals($expected, $actual, $description = null) { return $this->getScenario()->runStep(new Action('assertLessOrEquals', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * @param $actual * @param $description * @see \Codeception\Module\Asserts::assertIsEmpty() */ public function assertIsEmpty($actual, $description = null) { return $this->getScenario()->runStep(new Action('assertIsEmpty', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * @param $key * @param $actual * @param $description * @see \Codeception\Module\Asserts::assertArrayHasKey() */ public function assertArrayHasKey($key, $actual, $description = null) { return $this->getScenario()->runStep(new Action('assertArrayHasKey', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * @param $key * @param $actual * @param $description * @see \Codeception\Module\Asserts::assertArrayNotHasKey() */ public function assertArrayNotHasKey($key, $actual, $description = null) { return $this->getScenario()->runStep(new Action('assertArrayNotHasKey', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * Checks that array contains subset. * * @param array $subset * @param array $array * @param bool $strict * @param string $message * @see \Codeception\Module\Asserts::assertArraySubset() */ public function assertArraySubset($subset, $array, $strict = null, $message = null) { return $this->getScenario()->runStep(new Action('assertArraySubset', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * @param $expectedCount * @param $actual * @param $description * @see \Codeception\Module\Asserts::assertCount() */ public function assertCount($expectedCount, $actual, $description = null) { return $this->getScenario()->runStep(new Action('assertCount', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * @param $class * @param $actual * @param $description * @see \Codeception\Module\Asserts::assertInstanceOf() */ public function assertInstanceOf($class, $actual, $description = null) { return $this->getScenario()->runStep(new Action('assertInstanceOf', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * @param $class * @param $actual * @param $description * @see \Codeception\Module\Asserts::assertNotInstanceOf() */ public function assertNotInstanceOf($class, $actual, $description = null) { return $this->getScenario()->runStep(new Action('assertNotInstanceOf', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * @param $type * @param $actual * @param $description * @see \Codeception\Module\Asserts::assertInternalType() */ public function assertInternalType($type, $actual, $description = null) { return $this->getScenario()->runStep(new Action('assertInternalType', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * Fails the test with message. * * @param $message * @see \Codeception\Module\Asserts::fail() */ public function fail($message) { return $this->getScenario()->runStep(new Action('fail', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * Handles and checks exception called inside callback function. * Either exception class name or exception instance should be provided. * * ```php * expectException(MyException::class, function() { * $this->doSomethingBad(); * }); * * $I->expectException(new MyException(), function() { * $this->doSomethingBad(); * }); * ``` * If you want to check message or exception code, you can pass them with exception instance: * ```php * expectException(new MyException("Don't do bad things"), function() { * $this->doSomethingBad(); * }); * ``` * * @param $exception string or \Exception * @param $callback * * @deprecated Use expectThrowable instead * @see \Codeception\Module\Asserts::expectException() */ public function expectException($exception, $callback) { return $this->getScenario()->runStep(new Action('expectException', func_get_args())); } /** * [!] Method is generated. Documentation taken from corresponding module. * * Handles and checks throwables (Exceptions/Errors) called inside the callback function. * Either throwable class name or throwable instance should be provided. * * ```php * expectThrowable(MyThrowable::class, function() { * $this->doSomethingBad(); * }); * * $I->expectThrowable(new MyException(), function() { * $this->doSomethingBad(); * }); * ``` * If you want to check message or throwable code, you can pass them with throwable instance: * ```php * expectThrowable(new MyError("Don't do bad things"), function() { * $this->doSomethingBad(); * }); * ``` * * @param $throwable string or \Throwable * @param $callback * @see \Codeception\Module\Asserts::expectThrowable() */ public function expectThrowable($throwable, $callback) { return $this->getScenario()->runStep(new Action('expectThrowable', func_get_args())); } } ================================================ FILE: tests/unit/ConfigTest.php ================================================ 'test', 'redirectUrl' => 'http://google.com', 'privateKeyPath' => '/tmp', 'certPath' => '/tmp', 'scope' => ['test', 'test2', 'test3'], ]); $this->assertSame('test test2 test3', $config->getScopeString()); } /** * Data provider for @see ConfigTest::testConstruct() * * @return array */ public function dataProviderForConstructor(): array { return [ 'min' => [ [ 'clientId' => 'test', 'redirectUrl' => 'http://google.com', 'privateKeyPath' => '/tmp', 'certPath' => '/tmp', 'scope' => ['test', 'test2', 'test3'], ], null, ], 'max' => [ [ 'clientId' => 'test', 'redirectUrl' => 'http://google.com', 'privateKeyPath' => '/tmp', 'certPath' => '/tmp', 'portalUrl' => 'google.com', 'tokenUrlPath' => 'test', 'codeUrlPath' => 'test', 'personUrlPath' => 'test', 'logoutUrlPath' => 'test', 'privateKeyPassword' => 'test', 'oid' => 'test', 'responseType' => 'test', 'accessType' => 'test', 'tmpPath' => 'test', 'token' => 'test', 'scope' => ['test', 'test2', 'test3'], ], null, ], 'No cert path' => [ [ 'clientId' => 'test', 'redirectUrl' => 'http://google.com', 'privateKeyPath' => '/tmp', 'scope' => ['test', 'test2', 'test3'], ], InvalidConfigurationException::class, ], 'No private key path' => [ [ 'clientId' => 'test', 'redirectUrl' => 'http://google.com', 'certPath' => '/tmp', 'scope' => ['test', 'test2', 'test3'], ], InvalidConfigurationException::class, ], 'No redirect url' => [ [ 'clientId' => 'test', 'privateKeyPath' => '/tmp', 'certPath' => '/tmp', 'scope' => ['test', 'test2', 'test3'], ], InvalidConfigurationException::class, ], 'No client id' => [ [ 'redirectUrl' => 'http://google.com', 'privateKeyPath' => '/tmp', 'certPath' => '/tmp', 'scope' => ['test', 'test2', 'test3'], ], InvalidConfigurationException::class, ], 'invalid scope' => [ [ 'redirectUrl' => 'http://google.com', 'privateKeyPath' => '/tmp', 'certPath' => '/tmp', 'scope' => 'test test2 test3', ], InvalidConfigurationException::class, ], ]; } /** * @param $config * @param string|null $expectedException * @throws \Esia\Exceptions\InvalidConfigurationException * * @dataProvider dataProviderForConstructor */ public function testConstruct($config, string $expectedException = null): void { if ($expectedException) { $this->expectException($expectedException); } new Config($config); } /** * @throws InvalidConfigurationException */ public function testGetTokenUrl(): void { $config = new Config([ 'clientId' => 'test', 'redirectUrl' => 'http://google.com', 'privateKeyPath' => '/tmp', 'certPath' => '/tmp', 'portalUrl' => 'https://google.com/', 'tokenUrlPath' => 'test', 'scope' => ['test', 'test2', 'test3'], ]); $this->assertSame('https://google.com/test', $config->getTokenUrl()); } /** * @throws InvalidConfigurationException */ public function testGetCodeUrl(): void { $config = new Config([ 'clientId' => 'test', 'redirectUrl' => 'http://google.com', 'privateKeyPath' => '/tmp', 'certPath' => '/tmp', 'portalUrl' => 'https://google.com/', 'codeUrlPath' => 'test', 'scope' => ['test', 'test2', 'test3'], ]); $this->assertSame('https://google.com/test', $config->getCodeUrl()); } /** * @throws InvalidConfigurationException */ public function testGetPersonUrl(): void { $config = new Config([ 'clientId' => 'test', 'redirectUrl' => 'http://google.com', 'privateKeyPath' => '/tmp', 'certPath' => '/tmp', 'portalUrl' => 'https://google.com/', 'personUrlPath' => 'test', 'oid' => 'test', 'scope' => ['test', 'test2', 'test3'], ]); $this->assertSame('https://google.com/test/test', $config->getPersonUrl()); } /** * @throws InvalidConfigurationException */ public function testGetPersonUrlWithoutOid(): void { $config = new Config([ 'clientId' => 'test', 'redirectUrl' => 'http://google.com', 'privateKeyPath' => '/tmp', 'certPath' => '/tmp', 'portalUrl' => 'https://google.com/', 'personUrlPath' => 'test', 'scope' => ['test', 'test2', 'test3'], ]); $this->expectException(InvalidConfigurationException::class); $this->assertSame('https://google.com/test/test', $config->getPersonUrl()); } /** * @throws InvalidConfigurationException */ public function testGetLogoutUrl(): void { $config = new Config([ 'clientId' => 'test', 'redirectUrl' => 'http://google.com', 'privateKeyPath' => '/tmp', 'certPath' => '/tmp', 'portalUrl' => 'https://google.com/', 'logoutUrlPath' => 'test', 'scope' => ['test', 'test2', 'test3'], ]); $this->assertSame('https://google.com/test', $config->getLogoutUrl()); } } ================================================ FILE: tests/unit/Http/GuzzleHttpClientTest.php ================================================ $handler]); $client = new GuzzleHttpClient($guzzleClient); $response = $client->sendRequest(new Request('GET', '/')); self::assertSame(200, $response->getStatusCode()); $this->expectException(ClientExceptionInterface::class); $client->sendRequest(new Request('GET', '/')); } } ================================================ FILE: tests/unit/OpenIdCliOpensslTest.php ================================================ config = [ 'clientId' => 'INSP03211', 'redirectUrl' => 'http://my-site.com/response.php', 'portalUrl' => 'https://esia-portal1.test.gosuslugi.ru/', 'privateKeyPath' => codecept_data_dir('server-gost.key'), 'privateKeyPassword' => 'test', 'certPath' => codecept_data_dir('server-gost.crt'), 'tmpPath' => codecept_log_dir(), ]; $config = new Config($this->config); $this->openId = new OpenId($config); $this->openId->setSigner(new CliSignerPKCS7( $this->config['certPath'], $this->config['privateKeyPath'], $this->config['privateKeyPassword'], $this->config['tmpPath'] )); } /** * @throws AbstractEsiaException * @throws InvalidConfigurationException */ public function testGetToken(): void { $config = new Config($this->config); $oid = '123'; $oidBase64 = base64_encode('{ "urn:esia:sbj_id" : ' . $oid . '}'); $client = $this->buildClientWithResponses([ new Response(200, [], '{ "access_token": "test.' . $oidBase64 . '.test"}'), ]); $openId = new OpenId($config, $client); $openId->setSigner(new CliSignerPKCS7( $this->config['certPath'], $this->config['privateKeyPath'], $this->config['privateKeyPassword'], $this->config['tmpPath'] )); $token = $openId->getToken('test'); self::assertNotEmpty($token); self::assertSame($oid, $openId->getConfig()->getOid()); } } ================================================ FILE: tests/unit/OpenIdTest.php ================================================ config = [ 'clientId' => 'INSP03211', 'redirectUrl' => 'http://my-site.com/response.php', 'portalUrl' => 'https://esia-portal1.test.gosuslugi.ru/', 'privateKeyPath' => codecept_data_dir('server.key'), 'privateKeyPassword' => 'test', 'certPath' => codecept_data_dir('server.crt'), 'tmpPath' => codecept_log_dir(), ]; $config = new Config($this->config); $this->openId = new OpenId($config); } /** * @throws SignFailException * @throws AbstractEsiaException * @throws InvalidConfigurationException */ public function testGetToken(): void { $config = new Config($this->config); $oid = '123'; $oidBase64 = base64_encode('{ "urn:esia:sbj_id" : ' . $oid . '}'); $client = $this->buildClientWithResponses([ new Response(200, [], '{ "access_token": "test.' . $oidBase64 . '.test"}'), ]); $openId = new OpenId($config, $client); $token = $openId->getToken('test'); self::assertNotEmpty($token); self::assertSame($oid, $openId->getConfig()->getOid()); } /** * @throws InvalidConfigurationException * @throws AbstractEsiaException */ public function testGetPersonInfo(): void { $config = new Config($this->config); $oid = '123'; $config->setOid($oid); $config->setToken('test'); $client = $this->buildClientWithResponses([ new Response(200, [], '{"username": "test"}'), ]); $openId = new OpenId($config, $client); $info = $openId->getPersonInfo(); self::assertNotEmpty($info); self::assertSame(['username' => 'test'], $info); } /** * @throws InvalidConfigurationException * @throws AbstractEsiaException */ public function testGetContactInfo(): void { $config = new Config($this->config); $oid = '123'; $config->setOid($oid); $config->setToken('test'); $client = $this->buildClientWithResponses([ new Response(200, [], '{"size": 2, "elements": ["phone", "email"]}'), new Response(200, [], '{"phone": "555 555 555"}'), new Response(200, [], '{"email": "test@gmail.com"}'), ]); $openId = new OpenId($config, $client); $info = $openId->getContactInfo(); self::assertNotEmpty($info); self::assertSame([['phone' => '555 555 555'], ['email' => 'test@gmail.com']], $info); } /** * @throws InvalidConfigurationException * @throws AbstractEsiaException */ public function testGetAddressInfo(): void { $config = new Config($this->config); $oid = '123'; $config->setOid($oid); $config->setToken('test'); $client = $this->buildClientWithResponses([ new Response(200, [], '{"size": 2, "elements": ["phone", "email"]}'), new Response(200, [], '{"phone": "555 555 555"}'), new Response(200, [], '{"email": "test@gmail.com"}'), ]); $openId = new OpenId($config, $client); $info = $openId->getAddressInfo(); self::assertNotEmpty($info); self::assertSame([['phone' => '555 555 555'], ['email' => 'test@gmail.com']], $info); } /** * @throws InvalidConfigurationException * @throws AbstractEsiaException */ public function testGetDocInfo(): void { $config = new Config($this->config); $oid = '123'; $config->setOid($oid); $config->setToken('test'); $client = $this->buildClientWithResponses([ new Response(200, [], '{"size": 2, "elements": ["phone", "email"]}'), new Response(200, [], '{"phone": "555 555 555"}'), new Response(200, [], '{"email": "test@gmail.com"}'), ]); $openId = new OpenId($config, $client); $info = $openId->getDocInfo(); self::assertNotEmpty($info); self::assertSame([['phone' => '555 555 555'], ['email' => 'test@gmail.com']], $info); } /** * @throws InvalidConfigurationException */ public function testBuildLogoutUrl(): void { $config = $this->openId->getConfig(); $url = $config->getLogoutUrl() . '?client_id=' . $config->getClientId(); $logoutUrl = $this->openId->buildLogoutUrl(); self::assertSame($url, $logoutUrl); } /** * @throws InvalidConfigurationException */ public function testBuildLogoutUrlWithRedirect(): void { $config = $this->openId->getConfig(); $redirectUrl = 'test.example.com'; $url = $config->getLogoutUrl() . '?client_id=' . $config->getClientId() . '&redirect_url=' . $redirectUrl; $logoutUrl = $this->openId->buildLogoutUrl($redirectUrl); self::assertSame($url, $logoutUrl); } /** * Client with prepared responses * * @param array $responses * @return ClientInterface */ protected function buildClientWithResponses(array $responses): ClientInterface { $mock = new MockHandler($responses); $handler = HandlerStack::create($mock); $guzzleClient = new Client(['handler' => $handler]); return new GuzzleHttpClient($guzzleClient); } } ================================================ FILE: tests/unit/Signer/SignerPKCS7Test.php ================================================ sign('test'); self::assertNotEmpty($sign); } /** * @throws SignFailException */ public function testSignCertDoesNotExists(): void { $signer = new SignerPKCS7( '/test', codecept_data_dir('server.key'), 'test', codecept_log_dir() ); $this->expectException(NoSuchCertificateFileException::class); $signer->sign('test'); } /** * @throws SignFailException */ public function testPrivateKeyDoesNotExists(): void { $signer = new SignerPKCS7( codecept_data_dir('server.crt'), '/test', 'test', codecept_log_dir() ); $this->expectException(NoSuchKeyFileException::class); $signer->sign('test'); } /** * @throws SignFailException */ public function testTmpDirDoesNotExists(): void { $signer = new SignerPKCS7( codecept_data_dir('server.crt'), codecept_data_dir('server.key'), 'test', '/' ); $this->expectException(NoSuchTmpDirException::class); $signer->sign('test'); } /** * @throws SignFailException */ public function testTmpDirIsNotWritable(): void { $signer = new SignerPKCS7( codecept_data_dir('server.crt'), codecept_data_dir('server.key'), 'test', codecept_log_dir('non_writable_directory') ); $this->expectException(NoSuchTmpDirException::class); $signer->sign('test'); } /** * @throws SignFailException */ public function testCertificateIsNotReadable(): void { $signer = new SignerPKCS7( codecept_data_dir('non_readable_file'), codecept_data_dir('server.key'), 'test', codecept_log_dir() ); $this->expectException(CannotReadCertificateException::class); $signer->sign('test'); } /** * @throws SignFailException */ public function testPrivateKeyIsNotReadable(): void { $signer = new SignerPKCS7( codecept_data_dir('server.crt'), codecept_data_dir('non_readable_file'), 'test', codecept_log_dir() ); $this->expectException(CannotReadPrivateKeyException::class); $signer->sign('test'); } } ================================================ FILE: tests/unit/_bootstrap.php ================================================