├── .gitignore ├── .travis.yml ├── README.md ├── _config.yml ├── codeception.yml ├── composer.json ├── composer.lock ├── 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 ├── non_readable_file ├── server-gost.crt ├── server-gost.key ├── server.crt ├── server.csr └── server.key ├── _support ├── Helper │ └── Unit.php ├── UnitTester.php └── _generated │ └── UnitTesterActions.php ├── unit.suite.yml └── unit ├── ConfigTest.php ├── Http └── GuzzleHttpClientTest.php ├── OpenIdCliOpensslTest.php ├── OpenIdTest.php ├── Signer └── SignerPKCS7Test.php └── _bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | tests/tmp/* 3 | tests/_output/* 4 | public/* 5 | vendor 6 | 7 | tests/_data/non_readable_file -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: bionic 2 | language: php 3 | addons: 4 | apt: 5 | packages: 6 | - libengine-gost-openssl1.1 7 | 8 | before_install: 9 | - sudo bash tests/.configure-gost-openssl.sh 10 | 11 | php: 12 | - 7.1 13 | - 7.2 14 | - 7.3 15 | - 7.4 16 | - 8.0 17 | 18 | install: 19 | - travis_retry composer self-update 20 | - travis_retry composer --version 21 | - travis_retry composer update --prefer-dist --no-interaction 22 | 23 | script: 24 | - chmod 000 tests/_data/non_readable_file 25 | - php vendor/codeception/codeception/codecept run 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Единая система идентификации и аутентификации (ЕСИА) OpenId 3 | 4 | [![Build Status](https://travis-ci.org/fr05t1k/esia.svg?branch=master)](https://travis-ci.org/fr05t1k/esia) 5 | 6 | # Описание 7 | Компонент для авторизации на портале "Госуслуги". 8 | 9 | # Внимание! 10 | Получив токен вы можете выполнять любые API запросы. Библиотека не поддерживает все существующие методы в API, а предоставляет только самые базовые. Основная цель библиотеки - получение токена. 11 | 12 | # Установка 13 | 14 | При помощи [composer](https://getcomposer.org/download/): 15 | ``` 16 | composer require --prefer-dist fr05t1k/esia 17 | ``` 18 | Или добавьте в composer.json 19 | 20 | ``` 21 | "fr05t1k/esia" : "^2.0" 22 | ``` 23 | 24 | # Как использовать 25 | 26 | Пример получения ссылки для авторизации 27 | ```php 28 | 'INSP03211', 31 | 'redirectUrl' => 'http://my-site.com/response.php', 32 | 'portalUrl' => 'https://esia-portal1.test.gosuslugi.ru/', 33 | 'scope' => ['fullname', 'birthdate'], 34 | ]); 35 | $esia = new \Esia\OpenId($config); 36 | $esia->setSigner(new \Esia\Signer\SignerPKCS7( 37 | 'my-site.com.pem', 38 | 'my-site.com.pem', 39 | 'password', 40 | '/tmp' 41 | )); 42 | ?> 43 | 44 | Войти через портал госуслуги 45 | ``` 46 | 47 | После редиректа на ваш `redirectUrl` вы получите в `$_GET['code']` код для получения токена 48 | 49 | Пример получения токена и информации о пользователе 50 | 51 | ```php 52 | 53 | $esia = new \Esia\OpenId($config); 54 | 55 | // Вы можете использовать токен в дальнейшем вместе с oid 56 | $token = $esia->getToken($_GET['code']); 57 | 58 | $personInfo = $esia->getPersonInfo(); 59 | $addressInfo = $esia->getAddressInfo(); 60 | $contactInfo = $esia->getContactInfo(); 61 | $documentInfo = $esia->getDocInfo(); 62 | 63 | ``` 64 | # Конфиг 65 | 66 | `clientId` - ID вашего приложения. 67 | 68 | `redirectUrl` - URL куда будет перенаправлен ответ с кодом. 69 | 70 | `portalUrl` - по умолчанию: `https://esia-portal1.test.gosuslugi.ru/`. Домен портала для авторизация (только домен). 71 | 72 | `codeUrlPath` - по умолчанию: `aas/oauth2/ac`. URL для получения кода. 73 | 74 | `tokenUrlPath` - по умолчанию: `aas/oauth2/te`. URL для получение токена. 75 | 76 | `scope` - по умолчанию: `fullname birthdate gender email mobile id_doc snils inn`. Запрашиваемые права у пользователя. 77 | 78 | `privateKeyPath` - путь до приватного ключа. 79 | 80 | `privateKeyPassword` - пароль от приватного ключа. 81 | 82 | `certPath` - путь до сертификата. 83 | 84 | `tmpPath` - путь до дериктории где будет проходить подпись (должна быть доступна для записи). 85 | 86 | # Токен и oid 87 | 88 | Токен - jwt токен которые вы получаете от ЕСИА для дальнейшего взаимодействия 89 | 90 | oid - уникальный идентификатор владельца токена 91 | 92 | ## Как получить oid? 93 | Если 2 способа: 94 | 1. oid содержится в jwt токене, расшифровав его 95 | 2. После получения токена oid сохраняется в config и получить можно так 96 | ```php 97 | $esia->getConfig()->getOid(); 98 | ``` 99 | 100 | ## Переиспользование Токена 101 | 102 | Дополнительно укажите токен и идентификатор в конфиге 103 | ```php 104 | $config->setToken($jwt); 105 | $config->setOid($oid); 106 | ``` 107 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /codeception.yml: -------------------------------------------------------------------------------- 1 | actor: Tester 2 | paths: 3 | tests: tests 4 | log: tests/_output 5 | data: tests/_data 6 | support: tests/_support 7 | envs: tests/_envs 8 | bootstrap: _bootstrap.php 9 | settings: 10 | colors: true 11 | memory_limit: 1024M 12 | extensions: 13 | enabled: 14 | - Codeception\Extension\RunFailed 15 | coverage: 16 | enabled: true 17 | remote: false 18 | whitelist: 19 | include: 20 | - src/* 21 | 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fr05t1k/esia", 3 | "license": "MIT", 4 | "description": "OpenID ESIA authenticating", 5 | "keywords": [ 6 | "esia", 7 | "openid", 8 | "egov" 9 | ], 10 | "autoload": { 11 | "psr-4": { 12 | "Esia\\": "src/Esia" 13 | } 14 | }, 15 | "autoload-dev": { 16 | "psr-4": { 17 | "tests\\" : "tests" 18 | } 19 | }, 20 | "require": { 21 | "php": "^7.1|^8.0", 22 | "guzzlehttp/guzzle": "^6.1.0|^7.0", 23 | "psr/log": "^1.0", 24 | "psr/http-message": "^1.0", 25 | "psr/http-client": "^1.0" 26 | }, 27 | "suggest": { 28 | "ext-openssl": "SignerPKCS7 support" 29 | }, 30 | "require-dev": { 31 | "roave/security-advisories": "dev-latest", 32 | "codeception/codeception": "^4.0", 33 | "codeception/module-asserts": "^1.3" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Esia/Config.php: -------------------------------------------------------------------------------- 1 | clientId = $config['clientId'] ?? $this->clientId; 49 | if (!$this->clientId) { 50 | throw new InvalidConfigurationException('Please provide clientId'); 51 | } 52 | 53 | $this->redirectUrl = $config['redirectUrl'] ?? $this->redirectUrl; 54 | if (!$this->redirectUrl) { 55 | throw new InvalidConfigurationException('Please provide redirectUrl'); 56 | } 57 | 58 | $this->privateKeyPath = $config['privateKeyPath'] ?? $this->privateKeyPath; 59 | if (!$this->privateKeyPath) { 60 | throw new InvalidConfigurationException('Please provide privateKeyPath'); 61 | } 62 | $this->certPath = $config['certPath'] ?? $this->certPath; 63 | if (!$this->certPath) { 64 | throw new InvalidConfigurationException('Please provide certPath'); 65 | } 66 | 67 | $this->portalUrl = $config['portalUrl'] ?? $this->portalUrl; 68 | $this->tokenUrlPath = $config['tokenUrlPath'] ?? $this->tokenUrlPath; 69 | $this->codeUrlPath = $config['codeUrlPath'] ?? $this->codeUrlPath; 70 | $this->personUrlPath = $config['personUrlPath'] ?? $this->personUrlPath; 71 | $this->logoutUrlPath = $config['logoutUrlPath'] ?? $this->logoutUrlPath; 72 | $this->privateKeyPassword = $config['privateKeyPassword'] ?? $this->privateKeyPassword; 73 | $this->oid = $config['oid'] ?? $this->oid; 74 | $this->scope = $config['scope'] ?? $this->scope; 75 | if (!is_array($this->scope)) { 76 | throw new InvalidConfigurationException('scope must be array of strings'); 77 | } 78 | 79 | $this->responseType = $config['responseType'] ?? $this->responseType; 80 | $this->accessType = $config['accessType'] ?? $this->accessType; 81 | $this->tmpPath = $config['tmpPath'] ?? $this->tmpPath; 82 | $this->token = $config['token'] ?? $this->token; 83 | } 84 | 85 | public function getPortalUrl(): string 86 | { 87 | return $this->portalUrl; 88 | } 89 | 90 | public function getPrivateKeyPath(): string 91 | { 92 | return $this->privateKeyPath; 93 | } 94 | 95 | public function getPrivateKeyPassword(): string 96 | { 97 | return $this->privateKeyPassword; 98 | } 99 | 100 | public function getCertPath(): string 101 | { 102 | return $this->certPath; 103 | } 104 | 105 | public function getOid(): string 106 | { 107 | return $this->oid; 108 | } 109 | 110 | public function setOid(string $oid): void 111 | { 112 | $this->oid = $oid; 113 | } 114 | 115 | public function getScope(): array 116 | { 117 | return $this->scope; 118 | } 119 | 120 | public function getScopeString(): string 121 | { 122 | return implode(' ', $this->scope); 123 | } 124 | 125 | public function getResponseType(): string 126 | { 127 | return $this->responseType; 128 | } 129 | 130 | public function getAccessType(): string 131 | { 132 | return $this->accessType; 133 | } 134 | 135 | public function getTmpPath(): string 136 | { 137 | return $this->tmpPath; 138 | } 139 | 140 | public function getToken(): ?string 141 | { 142 | return $this->token; 143 | } 144 | 145 | public function setToken(string $token): void 146 | { 147 | $this->token = $token; 148 | } 149 | 150 | public function getClientId(): string 151 | { 152 | return $this->clientId; 153 | } 154 | 155 | public function getRedirectUrl(): string 156 | { 157 | return $this->redirectUrl; 158 | } 159 | 160 | /** 161 | * Return an url for request to get an access token 162 | */ 163 | public function getTokenUrl(): string 164 | { 165 | return $this->portalUrl . $this->tokenUrlPath; 166 | } 167 | 168 | /** 169 | * Return an url for request to get an authorization code 170 | */ 171 | public function getCodeUrl(): string 172 | { 173 | return $this->portalUrl . $this->codeUrlPath; 174 | } 175 | 176 | /** 177 | * @return string 178 | * @throws InvalidConfigurationException 179 | */ 180 | public function getPersonUrl(): string 181 | { 182 | if (!$this->oid) { 183 | throw new InvalidConfigurationException('Please provide oid'); 184 | } 185 | return $this->portalUrl . $this->personUrlPath . '/' . $this->oid; 186 | } 187 | 188 | /** 189 | * Return an url for logout 190 | */ 191 | public function getLogoutUrl(): string 192 | { 193 | return $this->portalUrl . $this->logoutUrlPath; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/Esia/Exceptions/AbstractEsiaException.php: -------------------------------------------------------------------------------- 1 | guzzle = $guzzle; 26 | } 27 | 28 | /** 29 | * Sends a PSR-7 request and returns a PSR-7 response. 30 | * 31 | * Every technically correct HTTP response MUST be returned as is, even if it represents a HTTP 32 | * error response or a redirect instruction. The only special case is 1xx responses, which MUST 33 | * be assembled in the HTTP client. 34 | * 35 | * The client MAY do modifications to the Request before sending it. Because PSR-7 objects are 36 | * immutable, one cannot assume that the object passed to ClientInterface::sendRequest() will be the same 37 | * object that is actually sent. For example the Request object that is returned by an exception MAY 38 | * be a different object than the one passed to sendRequest, so comparison by reference (===) is not possible. 39 | * 40 | * {@link https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message-meta.md#why-value-objects} 41 | * 42 | * @param RequestInterface $request 43 | * 44 | * @return ResponseInterface 45 | * 46 | * @throws ClientExceptionInterface If an error happens during processing the request. 47 | */ 48 | public function sendRequest(RequestInterface $request): ResponseInterface 49 | { 50 | try { 51 | return $this->guzzle->send($request); 52 | } catch (GuzzleException $e) { 53 | throw new HttpException($e->getMessage(), $e->getCode(), $e); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Esia/OpenId.php: -------------------------------------------------------------------------------- 1 | config = $config; 54 | $this->client = $client ?? new GuzzleHttpClient(new Client()); 55 | $this->logger = new NullLogger(); 56 | $this->signer = new SignerPKCS7( 57 | $config->getCertPath(), 58 | $config->getPrivateKeyPath(), 59 | $config->getPrivateKeyPassword(), 60 | $config->getTmpPath() 61 | ); 62 | } 63 | 64 | /** 65 | * Replace default signer 66 | */ 67 | public function setSigner(SignerInterface $signer): void 68 | { 69 | $this->signer = $signer; 70 | } 71 | 72 | /** 73 | * Get config 74 | */ 75 | public function getConfig(): Config 76 | { 77 | return $this->config; 78 | } 79 | 80 | /** 81 | * Return an url for authentication 82 | * 83 | * ```php 84 | * Login 85 | * ``` 86 | * 87 | * @return string|false 88 | * @throws SignFailException 89 | */ 90 | public function buildUrl() 91 | { 92 | $timestamp = $this->getTimeStamp(); 93 | $state = $this->buildState(); 94 | $message = $this->config->getScopeString() 95 | . $timestamp 96 | . $this->config->getClientId() 97 | . $state; 98 | 99 | $clientSecret = $this->signer->sign($message); 100 | 101 | $url = $this->config->getCodeUrl() . '?%s'; 102 | 103 | $params = [ 104 | 'client_id' => $this->config->getClientId(), 105 | 'client_secret' => $clientSecret, 106 | 'redirect_uri' => $this->config->getRedirectUrl(), 107 | 'scope' => $this->config->getScopeString(), 108 | 'response_type' => $this->config->getResponseType(), 109 | 'state' => $state, 110 | 'access_type' => $this->config->getAccessType(), 111 | 'timestamp' => $timestamp, 112 | ]; 113 | 114 | $request = http_build_query($params); 115 | 116 | return sprintf($url, $request); 117 | } 118 | 119 | /** 120 | * Return an url for logout 121 | */ 122 | public function buildLogoutUrl(string $redirectUrl = null): string 123 | { 124 | $url = $this->config->getLogoutUrl() . '?%s'; 125 | $params = [ 126 | 'client_id' => $this->config->getClientId(), 127 | ]; 128 | 129 | if ($redirectUrl) { 130 | $params['redirect_url'] = $redirectUrl; 131 | } 132 | 133 | $request = http_build_query($params); 134 | 135 | return sprintf($url, $request); 136 | } 137 | 138 | /** 139 | * Method collect a token with given code 140 | * 141 | * @throws SignFailException 142 | * @throws AbstractEsiaException 143 | */ 144 | public function getToken(string $code): string 145 | { 146 | $timestamp = $this->getTimeStamp(); 147 | $state = $this->buildState(); 148 | 149 | $clientSecret = $this->signer->sign( 150 | $this->config->getScopeString() 151 | . $timestamp 152 | . $this->config->getClientId() 153 | . $state 154 | ); 155 | 156 | $body = [ 157 | 'client_id' => $this->config->getClientId(), 158 | 'code' => $code, 159 | 'grant_type' => 'authorization_code', 160 | 'client_secret' => $clientSecret, 161 | 'state' => $state, 162 | 'redirect_uri' => $this->config->getRedirectUrl(), 163 | 'scope' => $this->config->getScopeString(), 164 | 'timestamp' => $timestamp, 165 | 'token_type' => 'Bearer', 166 | 'refresh_token' => $state, 167 | ]; 168 | 169 | $payload = $this->sendRequest( 170 | new Request( 171 | 'POST', 172 | $this->config->getTokenUrl(), 173 | [ 174 | 'Content-Type' => 'application/x-www-form-urlencoded', 175 | ], 176 | http_build_query($body) 177 | ) 178 | ); 179 | 180 | $this->logger->debug('Payload: ', $payload); 181 | 182 | $token = $payload['access_token']; 183 | $this->config->setToken($token); 184 | 185 | # get object id from token 186 | $chunks = explode('.', $token); 187 | $payload = json_decode($this->base64UrlSafeDecode($chunks[1]), true); 188 | $this->config->setOid($payload['urn:esia:sbj_id']); 189 | 190 | return $token; 191 | } 192 | 193 | /** 194 | * Fetch person info from current person 195 | * 196 | * You must collect token person before 197 | * calling this method 198 | * 199 | * @throws AbstractEsiaException 200 | */ 201 | public function getPersonInfo(): array 202 | { 203 | $url = $this->config->getPersonUrl(); 204 | 205 | return $this->sendRequest(new Request('GET', $url)); 206 | } 207 | 208 | /** 209 | * Fetch contact info about current person 210 | * 211 | * You must collect token person before 212 | * calling this method 213 | * 214 | * @throws Exceptions\InvalidConfigurationException 215 | * @throws AbstractEsiaException 216 | */ 217 | public function getContactInfo(): array 218 | { 219 | $url = $this->config->getPersonUrl() . '/ctts'; 220 | $payload = $this->sendRequest(new Request('GET', $url)); 221 | 222 | if ($payload && $payload['size'] > 0) { 223 | return $this->collectArrayElements($payload['elements']); 224 | } 225 | 226 | return $payload; 227 | } 228 | 229 | 230 | /** 231 | * Fetch address from current person 232 | * 233 | * You must collect token person before 234 | * calling this method 235 | * 236 | * @throws Exceptions\InvalidConfigurationException 237 | * @throws AbstractEsiaException 238 | */ 239 | public function getAddressInfo(): array 240 | { 241 | $url = $this->config->getPersonUrl() . '/addrs'; 242 | $payload = $this->sendRequest(new Request('GET', $url)); 243 | 244 | if ($payload['size'] > 0) { 245 | return $this->collectArrayElements($payload['elements']); 246 | } 247 | 248 | return $payload; 249 | } 250 | 251 | /** 252 | * Fetch documents info about current person 253 | * 254 | * You must collect token person before 255 | * calling this method 256 | * 257 | * @throws Exceptions\InvalidConfigurationException 258 | * @throws AbstractEsiaException 259 | */ 260 | public function getDocInfo(): array 261 | { 262 | $url = $this->config->getPersonUrl() . '/docs'; 263 | 264 | $payload = $this->sendRequest(new Request('GET', $url)); 265 | 266 | if ($payload && $payload['size'] > 0) { 267 | return $this->collectArrayElements($payload['elements']); 268 | } 269 | 270 | return $payload; 271 | } 272 | 273 | /** 274 | * This method can iterate on each element 275 | * and fetch entities from esia by url 276 | * 277 | * @throws AbstractEsiaException 278 | */ 279 | private function collectArrayElements($elements): array 280 | { 281 | $result = []; 282 | foreach ($elements as $elementUrl) { 283 | $elementPayload = $this->sendRequest(new Request('GET', $elementUrl)); 284 | 285 | if ($elementPayload) { 286 | $result[] = $elementPayload; 287 | } 288 | } 289 | 290 | return $result; 291 | } 292 | 293 | /** 294 | * @throws AbstractEsiaException 295 | */ 296 | private function sendRequest(RequestInterface $request): array 297 | { 298 | try { 299 | if ($this->config->getToken()) { 300 | /** @noinspection CallableParameterUseCaseInTypeContextInspection */ 301 | $request = $request->withHeader('Authorization', 'Bearer ' . $this->config->getToken()); 302 | } 303 | $response = $this->client->sendRequest($request); 304 | $responseBody = json_decode($response->getBody()->getContents(), true); 305 | 306 | if (!is_array($responseBody)) { 307 | throw new RuntimeException( 308 | sprintf( 309 | 'Cannot decode response body. JSON error (%d): %s', 310 | json_last_error(), 311 | json_last_error_msg() 312 | ) 313 | ); 314 | } 315 | 316 | return $responseBody; 317 | } catch (ClientExceptionInterface $e) { 318 | $this->logger->error('Request was failed', ['exception' => $e]); 319 | $prev = $e->getPrevious(); 320 | 321 | // Only for Guzzle 322 | if ($prev instanceof BadResponseException 323 | && $prev->getResponse() !== null 324 | && $prev->getResponse()->getStatusCode() === 403 325 | ) { 326 | throw new ForbiddenException('Request is forbidden', 0, $e); 327 | } 328 | 329 | throw new RequestFailException('Request is failed', 0, $e); 330 | } catch (RuntimeException $e) { 331 | $this->logger->error('Cannot read body', ['exception' => $e]); 332 | throw new RequestFailException('Cannot read body', 0, $e); 333 | } catch (InvalidArgumentException $e) { 334 | $this->logger->error('Wrong header', ['exception' => $e]); 335 | throw new RequestFailException('Wrong header', 0, $e); 336 | } 337 | } 338 | 339 | private function getTimeStamp(): string 340 | { 341 | return date('Y.m.d H:i:s O'); 342 | } 343 | 344 | /** 345 | * Generate state with uuid 346 | * 347 | * @throws SignFailException 348 | */ 349 | private function buildState(): string 350 | { 351 | try { 352 | return sprintf( 353 | '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', 354 | random_int(0, 0xffff), 355 | random_int(0, 0xffff), 356 | random_int(0, 0xffff), 357 | random_int(0, 0x0fff) | 0x4000, 358 | random_int(0, 0x3fff) | 0x8000, 359 | random_int(0, 0xffff), 360 | random_int(0, 0xffff), 361 | random_int(0, 0xffff) 362 | ); 363 | } catch (Exception $e) { 364 | throw new CannotGenerateRandomIntException('Cannot generate random integer', $e); 365 | } 366 | } 367 | 368 | /** 369 | * Url safe for base64 370 | */ 371 | private function base64UrlSafeDecode(string $string): string 372 | { 373 | $base64 = strtr($string, '-_', '+/'); 374 | 375 | return base64_decode($base64); 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /src/Esia/Signer/AbstractSignerPKCS7.php: -------------------------------------------------------------------------------- 1 | certPath = $certPath; 49 | $this->privateKeyPath = $privateKeyPath; 50 | $this->privateKeyPassword = $privateKeyPassword; 51 | $this->tmpPath = $tmpPath; 52 | $this->logger = new NullLogger(); 53 | } 54 | 55 | /** 56 | * Temporary directory for message signing (must me writable) 57 | * 58 | * @var string 59 | */ 60 | protected $tmpPath; 61 | 62 | /** 63 | * @throws SignFailException 64 | */ 65 | protected function checkFilesExists(): void 66 | { 67 | if (!file_exists($this->certPath)) { 68 | throw new NoSuchCertificateFileException('Certificate does not exist'); 69 | } 70 | if (!is_readable($this->certPath)) { 71 | throw new CannotReadCertificateException('Cannot read the certificate'); 72 | } 73 | if (!file_exists($this->privateKeyPath)) { 74 | throw new NoSuchKeyFileException('Private key does not exist'); 75 | } 76 | if (!is_readable($this->privateKeyPath)) { 77 | throw new CannotReadPrivateKeyException('Cannot read the private key'); 78 | } 79 | if (!file_exists($this->tmpPath)) { 80 | throw new NoSuchTmpDirException('Temporary folder is not found'); 81 | } 82 | if (!is_writable($this->tmpPath)) { 83 | throw new NoSuchTmpDirException('Temporary folder is not writable'); 84 | } 85 | } 86 | 87 | /** 88 | * Generate random unique string 89 | */ 90 | protected function getRandomString(): string 91 | { 92 | return md5(uniqid(mt_rand(), true)); 93 | } 94 | 95 | /** 96 | * Url safe for base64 97 | */ 98 | protected function urlSafe(string $string): string 99 | { 100 | return rtrim(strtr(trim($string), '+/', '-_'), '='); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Esia/Signer/CliSignerPKCS7.php: -------------------------------------------------------------------------------- 1 | checkFilesExists(); 15 | 16 | // random unique directories for sign 17 | $messageFile = $this->tmpPath . DIRECTORY_SEPARATOR . $this->getRandomString(); 18 | $signFile = $this->tmpPath . DIRECTORY_SEPARATOR . $this->getRandomString(); 19 | file_put_contents($messageFile, $message); 20 | 21 | $this->run( 22 | 'openssl ' . 23 | 'smime -engine gost -sign -binary -outform DER -noattr ' . 24 | '-signer ' . escapeshellarg($this->certPath) . ' ' . 25 | '-inkey ' . escapeshellarg($this->privateKeyPath) . ' ' . 26 | '-passin ' . escapeshellarg('pass:' . $this->privateKeyPassword) . ' ' . 27 | '-in ' . escapeshellarg($messageFile) . ' ' . 28 | '-out ' . escapeshellarg($signFile) 29 | ); 30 | 31 | $signed = file_get_contents($signFile); 32 | if ($signed === false) { 33 | $message = sprintf('cannot read %s file', $signFile); 34 | $this->logger->error($message); 35 | throw new SignFailException($message); 36 | } 37 | $sign = $this->urlSafe(base64_encode($signed)); 38 | 39 | unlink($signFile); 40 | unlink($messageFile); 41 | return $sign; 42 | } 43 | 44 | /** 45 | * @throws SignFailException 46 | */ 47 | private function run(string $command): void 48 | { 49 | $process = proc_open( 50 | $command, 51 | [ 52 | ['pipe', 'w'], // stdout 53 | ['pipe', 'w'], // stderr 54 | ], 55 | $pipes 56 | ); 57 | 58 | $result = stream_get_contents($pipes[0]); 59 | fclose($pipes[0]); 60 | 61 | $errors = stream_get_contents($pipes[1]); 62 | fclose($pipes[1]); 63 | 64 | $code = proc_close($process); 65 | if (0 !== $code || $result === false) { 66 | $errors = $errors ?: 'unknown'; 67 | $this->logger->error('Sign fail'); 68 | $this->logger->error('SSL error: ' . $errors); 69 | throw new SignFailException($errors); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Esia/Signer/Exceptions/CannotGenerateRandomIntException.php: -------------------------------------------------------------------------------- 1 | pkcs7Flags |= $pkcs7Flag; 16 | } 17 | 18 | /** 19 | * @throws SignFailException 20 | */ 21 | public function sign(string $message): string 22 | { 23 | $this->checkFilesExists(); 24 | 25 | $certContent = file_get_contents($this->certPath); 26 | $keyContent = file_get_contents($this->privateKeyPath); 27 | 28 | $cert = openssl_x509_read($certContent); 29 | 30 | if ($cert === false) { 31 | throw new CannotReadCertificateException('Cannot read the certificate: ' . openssl_error_string()); 32 | } 33 | 34 | $this->logger->debug('Cert: ' . print_r($cert, true), ['cert' => $cert]); 35 | 36 | $privateKey = openssl_pkey_get_private($keyContent, $this->privateKeyPassword); 37 | 38 | if ($privateKey === false) { 39 | throw new CannotReadPrivateKeyException('Cannot read the private key: ' . openssl_error_string()); 40 | } 41 | 42 | $this->logger->debug('Private key: : ' . print_r($privateKey, true), ['privateKey' => $privateKey]); 43 | 44 | // random unique directories for sign 45 | $messageFile = $this->tmpPath . DIRECTORY_SEPARATOR . $this->getRandomString(); 46 | $signFile = $this->tmpPath . DIRECTORY_SEPARATOR . $this->getRandomString(); 47 | file_put_contents($messageFile, $message); 48 | $signResult = openssl_pkcs7_sign( 49 | $messageFile, 50 | $signFile, 51 | $cert, 52 | $privateKey, 53 | [], 54 | $this->pkcs7Flags 55 | ); 56 | 57 | if ($signResult) { 58 | $this->logger->debug('Sign success'); 59 | } else { 60 | $this->logger->error('Sign fail'); 61 | $this->logger->error('SSL error: ' . openssl_error_string()); 62 | throw new SignFailException('Cannot sign the message'); 63 | } 64 | 65 | $signed = file_get_contents($signFile); 66 | 67 | # split by section 68 | $signed = explode("\n\n", $signed); 69 | 70 | # get third section which contains sign and join into one line 71 | $sign = str_replace("\n", '', $this->urlSafe($signed[3])); 72 | 73 | unlink($signFile); 74 | unlink($messageFile); 75 | 76 | return $sign; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/.configure-gost-openssl.sh: -------------------------------------------------------------------------------- 1 | sed -i '1iopenssl_conf=openssl_def' /usr/lib/ssl/openssl.cnf 2 | tee -a /usr/lib/ssl/openssl.cnf <assertEquals(5, $element->getChildrenCount()); 31 | * ``` 32 | * 33 | * Floating-point example: 34 | * ```php 35 | * assertEquals(0.3, $calculator->add(0.1, 0.2), 'Calculator should add the two numbers correctly.', 0.01); 37 | * ``` 38 | * 39 | * @param $expected 40 | * @param $actual 41 | * @param string $message 42 | * @param float $delta 43 | * @see \Codeception\Module\Asserts::assertEquals() 44 | */ 45 | public function assertEquals($expected, $actual, $message = null, $delta = null) { 46 | return $this->getScenario()->runStep(new Action('assertEquals', func_get_args())); 47 | } 48 | 49 | 50 | /** 51 | * [!] Method is generated. Documentation taken from corresponding module. 52 | * 53 | * Checks that two variables are not equal. If you're comparing floating-point values, 54 | * you can specify the optional "delta" parameter which dictates how great of a precision 55 | * error are you willing to tolerate in order to consider the two values not equal. 56 | * 57 | * Regular example: 58 | * ```php 59 | * assertNotEquals(0, $element->getChildrenCount()); 61 | * ``` 62 | * 63 | * Floating-point example: 64 | * ```php 65 | * assertNotEquals(0.4, $calculator->add(0.1, 0.2), 'Calculator should add the two numbers correctly.', 0.01); 67 | * ``` 68 | * 69 | * @param $expected 70 | * @param $actual 71 | * @param string $message 72 | * @param float $delta 73 | * @see \Codeception\Module\Asserts::assertNotEquals() 74 | */ 75 | public function assertNotEquals($expected, $actual, $message = null, $delta = null) { 76 | return $this->getScenario()->runStep(new Action('assertNotEquals', func_get_args())); 77 | } 78 | 79 | 80 | /** 81 | * [!] Method is generated. Documentation taken from corresponding module. 82 | * 83 | * Checks that two variables are same 84 | * 85 | * @param $expected 86 | * @param $actual 87 | * @param string $message 88 | * @see \Codeception\Module\Asserts::assertSame() 89 | */ 90 | public function assertSame($expected, $actual, $message = null) { 91 | return $this->getScenario()->runStep(new Action('assertSame', func_get_args())); 92 | } 93 | 94 | 95 | /** 96 | * [!] Method is generated. Documentation taken from corresponding module. 97 | * 98 | * Checks that two variables are not same 99 | * 100 | * @param $expected 101 | * @param $actual 102 | * @param string $message 103 | * @see \Codeception\Module\Asserts::assertNotSame() 104 | */ 105 | public function assertNotSame($expected, $actual, $message = null) { 106 | return $this->getScenario()->runStep(new Action('assertNotSame', func_get_args())); 107 | } 108 | 109 | 110 | /** 111 | * [!] Method is generated. Documentation taken from corresponding module. 112 | * 113 | * Checks that actual is greater than expected 114 | * 115 | * @param $expected 116 | * @param $actual 117 | * @param string $message 118 | * @see \Codeception\Module\Asserts::assertGreaterThan() 119 | */ 120 | public function assertGreaterThan($expected, $actual, $message = null) { 121 | return $this->getScenario()->runStep(new Action('assertGreaterThan', func_get_args())); 122 | } 123 | 124 | 125 | /** 126 | * [!] Method is generated. Documentation taken from corresponding module. 127 | * 128 | * Checks that actual is greater or equal than expected 129 | * 130 | * @param $expected 131 | * @param $actual 132 | * @param string $message 133 | * @see \Codeception\Module\Asserts::assertGreaterThanOrEqual() 134 | */ 135 | public function assertGreaterThanOrEqual($expected, $actual, $message = null) { 136 | return $this->getScenario()->runStep(new Action('assertGreaterThanOrEqual', func_get_args())); 137 | } 138 | 139 | 140 | /** 141 | * [!] Method is generated. Documentation taken from corresponding module. 142 | * 143 | * Checks that actual is less than expected 144 | * 145 | * @param $expected 146 | * @param $actual 147 | * @param string $message 148 | * @see \Codeception\Module\Asserts::assertLessThan() 149 | */ 150 | public function assertLessThan($expected, $actual, $message = null) { 151 | return $this->getScenario()->runStep(new Action('assertLessThan', func_get_args())); 152 | } 153 | 154 | 155 | /** 156 | * [!] Method is generated. Documentation taken from corresponding module. 157 | * 158 | * Checks that actual is less or equal than expected 159 | * 160 | * @param $expected 161 | * @param $actual 162 | * @param string $message 163 | * @see \Codeception\Module\Asserts::assertLessThanOrEqual() 164 | */ 165 | public function assertLessThanOrEqual($expected, $actual, $message = null) { 166 | return $this->getScenario()->runStep(new Action('assertLessThanOrEqual', func_get_args())); 167 | } 168 | 169 | 170 | /** 171 | * [!] Method is generated. Documentation taken from corresponding module. 172 | * 173 | * Checks that haystack contains needle 174 | * 175 | * @param $needle 176 | * @param $haystack 177 | * @param string $message 178 | * @see \Codeception\Module\Asserts::assertContains() 179 | */ 180 | public function assertContains($needle, $haystack, $message = null) { 181 | return $this->getScenario()->runStep(new Action('assertContains', func_get_args())); 182 | } 183 | 184 | 185 | /** 186 | * [!] Method is generated. Documentation taken from corresponding module. 187 | * 188 | * Checks that haystack doesn't contain needle. 189 | * 190 | * @param $needle 191 | * @param $haystack 192 | * @param string $message 193 | * @see \Codeception\Module\Asserts::assertNotContains() 194 | */ 195 | public function assertNotContains($needle, $haystack, $message = null) { 196 | return $this->getScenario()->runStep(new Action('assertNotContains', func_get_args())); 197 | } 198 | 199 | 200 | /** 201 | * [!] Method is generated. Documentation taken from corresponding module. 202 | * 203 | * Checks that string match with pattern 204 | * 205 | * @param string $pattern 206 | * @param string $string 207 | * @param string $message 208 | * @see \Codeception\Module\Asserts::assertRegExp() 209 | */ 210 | public function assertRegExp($pattern, $string, $message = null) { 211 | return $this->getScenario()->runStep(new Action('assertRegExp', func_get_args())); 212 | } 213 | 214 | 215 | /** 216 | * [!] Method is generated. Documentation taken from corresponding module. 217 | * 218 | * Checks that string not match with pattern 219 | * 220 | * @param string $pattern 221 | * @param string $string 222 | * @param string $message 223 | * @see \Codeception\Module\Asserts::assertNotRegExp() 224 | */ 225 | public function assertNotRegExp($pattern, $string, $message = null) { 226 | return $this->getScenario()->runStep(new Action('assertNotRegExp', func_get_args())); 227 | } 228 | 229 | 230 | /** 231 | * [!] Method is generated. Documentation taken from corresponding module. 232 | * 233 | * Checks that a string starts with the given prefix. 234 | * 235 | * @param string $prefix 236 | * @param string $string 237 | * @param string $message 238 | * @see \Codeception\Module\Asserts::assertStringStartsWith() 239 | */ 240 | public function assertStringStartsWith($prefix, $string, $message = null) { 241 | return $this->getScenario()->runStep(new Action('assertStringStartsWith', func_get_args())); 242 | } 243 | 244 | 245 | /** 246 | * [!] Method is generated. Documentation taken from corresponding module. 247 | * 248 | * Checks that a string doesn't start with the given prefix. 249 | * 250 | * @param string $prefix 251 | * @param string $string 252 | * @param string $message 253 | * @see \Codeception\Module\Asserts::assertStringStartsNotWith() 254 | */ 255 | public function assertStringStartsNotWith($prefix, $string, $message = null) { 256 | return $this->getScenario()->runStep(new Action('assertStringStartsNotWith', func_get_args())); 257 | } 258 | 259 | 260 | /** 261 | * [!] Method is generated. Documentation taken from corresponding module. 262 | * 263 | * Checks that variable is empty. 264 | * 265 | * @param $actual 266 | * @param string $message 267 | * @see \Codeception\Module\Asserts::assertEmpty() 268 | */ 269 | public function assertEmpty($actual, $message = null) { 270 | return $this->getScenario()->runStep(new Action('assertEmpty', func_get_args())); 271 | } 272 | 273 | 274 | /** 275 | * [!] Method is generated. Documentation taken from corresponding module. 276 | * 277 | * Checks that variable is not empty. 278 | * 279 | * @param $actual 280 | * @param string $message 281 | * @see \Codeception\Module\Asserts::assertNotEmpty() 282 | */ 283 | public function assertNotEmpty($actual, $message = null) { 284 | return $this->getScenario()->runStep(new Action('assertNotEmpty', func_get_args())); 285 | } 286 | 287 | 288 | /** 289 | * [!] Method is generated. Documentation taken from corresponding module. 290 | * 291 | * Checks that variable is NULL 292 | * 293 | * @param $actual 294 | * @param string $message 295 | * @see \Codeception\Module\Asserts::assertNull() 296 | */ 297 | public function assertNull($actual, $message = null) { 298 | return $this->getScenario()->runStep(new Action('assertNull', func_get_args())); 299 | } 300 | 301 | 302 | /** 303 | * [!] Method is generated. Documentation taken from corresponding module. 304 | * 305 | * Checks that variable is not NULL 306 | * 307 | * @param $actual 308 | * @param string $message 309 | * @see \Codeception\Module\Asserts::assertNotNull() 310 | */ 311 | public function assertNotNull($actual, $message = null) { 312 | return $this->getScenario()->runStep(new Action('assertNotNull', func_get_args())); 313 | } 314 | 315 | 316 | /** 317 | * [!] Method is generated. Documentation taken from corresponding module. 318 | * 319 | * Checks that condition is positive. 320 | * 321 | * @param $condition 322 | * @param string $message 323 | * @see \Codeception\Module\Asserts::assertTrue() 324 | */ 325 | public function assertTrue($condition, $message = null) 326 | { 327 | return $this->getScenario()->runStep(new Action('assertTrue', func_get_args())); 328 | } 329 | 330 | 331 | /** 332 | * [!] Method is generated. Documentation taken from corresponding module. 333 | * 334 | * Checks that the condition is NOT true (everything but true) 335 | * 336 | * @param $condition 337 | * @param string $message 338 | * @see \Codeception\Module\Asserts::assertNotTrue() 339 | */ 340 | public function assertNotTrue($condition, $message = null) 341 | { 342 | return $this->getScenario()->runStep(new Action('assertNotTrue', func_get_args())); 343 | } 344 | 345 | 346 | /** 347 | * [!] Method is generated. Documentation taken from corresponding module. 348 | * 349 | * Checks that condition is negative. 350 | * 351 | * @param $condition 352 | * @param string $message 353 | * @see \Codeception\Module\Asserts::assertFalse() 354 | */ 355 | public function assertFalse($condition, $message = null) 356 | { 357 | return $this->getScenario()->runStep(new Action('assertFalse', func_get_args())); 358 | } 359 | 360 | 361 | /** 362 | * [!] Method is generated. Documentation taken from corresponding module. 363 | * 364 | * Checks that the condition is NOT false (everything but false) 365 | * 366 | * @param $condition 367 | * @param string $message 368 | * @see \Codeception\Module\Asserts::assertNotFalse() 369 | */ 370 | public function assertNotFalse($condition, $message = null) 371 | { 372 | return $this->getScenario()->runStep(new Action('assertNotFalse', func_get_args())); 373 | } 374 | 375 | 376 | /** 377 | * [!] Method is generated. Documentation taken from corresponding module. 378 | * 379 | * Checks if file exists 380 | * 381 | * @param string $filename 382 | * @param string $message 383 | * @see \Codeception\Module\Asserts::assertFileExists() 384 | */ 385 | public function assertFileExists($filename, $message = null) 386 | { 387 | return $this->getScenario()->runStep(new Action('assertFileExists', func_get_args())); 388 | } 389 | 390 | 391 | /** 392 | * [!] Method is generated. Documentation taken from corresponding module. 393 | * 394 | * Checks if file doesn't exist 395 | * 396 | * @param string $filename 397 | * @param string $message 398 | * @see \Codeception\Module\Asserts::assertFileNotExists() 399 | */ 400 | public function assertFileNotExists($filename, $message = null) { 401 | return $this->getScenario()->runStep(new Action('assertFileNotExists', func_get_args())); 402 | } 403 | 404 | 405 | /** 406 | * [!] Method is generated. Documentation taken from corresponding module. 407 | * 408 | * @param $expected 409 | * @param $actual 410 | * @param $description 411 | * @see \Codeception\Module\Asserts::assertGreaterOrEquals() 412 | */ 413 | public function assertGreaterOrEquals($expected, $actual, $description = null) { 414 | return $this->getScenario()->runStep(new Action('assertGreaterOrEquals', func_get_args())); 415 | } 416 | 417 | 418 | /** 419 | * [!] Method is generated. Documentation taken from corresponding module. 420 | * 421 | * @param $expected 422 | * @param $actual 423 | * @param $description 424 | * @see \Codeception\Module\Asserts::assertLessOrEquals() 425 | */ 426 | public function assertLessOrEquals($expected, $actual, $description = null) { 427 | return $this->getScenario()->runStep(new Action('assertLessOrEquals', func_get_args())); 428 | } 429 | 430 | 431 | /** 432 | * [!] Method is generated. Documentation taken from corresponding module. 433 | * 434 | * @param $actual 435 | * @param $description 436 | * @see \Codeception\Module\Asserts::assertIsEmpty() 437 | */ 438 | public function assertIsEmpty($actual, $description = null) { 439 | return $this->getScenario()->runStep(new Action('assertIsEmpty', func_get_args())); 440 | } 441 | 442 | 443 | /** 444 | * [!] Method is generated. Documentation taken from corresponding module. 445 | * 446 | * @param $key 447 | * @param $actual 448 | * @param $description 449 | * @see \Codeception\Module\Asserts::assertArrayHasKey() 450 | */ 451 | public function assertArrayHasKey($key, $actual, $description = null) { 452 | return $this->getScenario()->runStep(new Action('assertArrayHasKey', func_get_args())); 453 | } 454 | 455 | 456 | /** 457 | * [!] Method is generated. Documentation taken from corresponding module. 458 | * 459 | * @param $key 460 | * @param $actual 461 | * @param $description 462 | * @see \Codeception\Module\Asserts::assertArrayNotHasKey() 463 | */ 464 | public function assertArrayNotHasKey($key, $actual, $description = null) { 465 | return $this->getScenario()->runStep(new Action('assertArrayNotHasKey', func_get_args())); 466 | } 467 | 468 | 469 | /** 470 | * [!] Method is generated. Documentation taken from corresponding module. 471 | * 472 | * Checks that array contains subset. 473 | * 474 | * @param array $subset 475 | * @param array $array 476 | * @param bool $strict 477 | * @param string $message 478 | * @see \Codeception\Module\Asserts::assertArraySubset() 479 | */ 480 | public function assertArraySubset($subset, $array, $strict = null, $message = null) { 481 | return $this->getScenario()->runStep(new Action('assertArraySubset', func_get_args())); 482 | } 483 | 484 | 485 | /** 486 | * [!] Method is generated. Documentation taken from corresponding module. 487 | * 488 | * @param $expectedCount 489 | * @param $actual 490 | * @param $description 491 | * @see \Codeception\Module\Asserts::assertCount() 492 | */ 493 | public function assertCount($expectedCount, $actual, $description = null) { 494 | return $this->getScenario()->runStep(new Action('assertCount', func_get_args())); 495 | } 496 | 497 | 498 | /** 499 | * [!] Method is generated. Documentation taken from corresponding module. 500 | * 501 | * @param $class 502 | * @param $actual 503 | * @param $description 504 | * @see \Codeception\Module\Asserts::assertInstanceOf() 505 | */ 506 | public function assertInstanceOf($class, $actual, $description = null) { 507 | return $this->getScenario()->runStep(new Action('assertInstanceOf', func_get_args())); 508 | } 509 | 510 | 511 | /** 512 | * [!] Method is generated. Documentation taken from corresponding module. 513 | * 514 | * @param $class 515 | * @param $actual 516 | * @param $description 517 | * @see \Codeception\Module\Asserts::assertNotInstanceOf() 518 | */ 519 | public function assertNotInstanceOf($class, $actual, $description = null) { 520 | return $this->getScenario()->runStep(new Action('assertNotInstanceOf', func_get_args())); 521 | } 522 | 523 | 524 | /** 525 | * [!] Method is generated. Documentation taken from corresponding module. 526 | * 527 | * @param $type 528 | * @param $actual 529 | * @param $description 530 | * @see \Codeception\Module\Asserts::assertInternalType() 531 | */ 532 | public function assertInternalType($type, $actual, $description = null) { 533 | return $this->getScenario()->runStep(new Action('assertInternalType', func_get_args())); 534 | } 535 | 536 | 537 | /** 538 | * [!] Method is generated. Documentation taken from corresponding module. 539 | * 540 | * Fails the test with message. 541 | * 542 | * @param $message 543 | * @see \Codeception\Module\Asserts::fail() 544 | */ 545 | public function fail($message) { 546 | return $this->getScenario()->runStep(new Action('fail', func_get_args())); 547 | } 548 | 549 | 550 | /** 551 | * [!] Method is generated. Documentation taken from corresponding module. 552 | * 553 | * Handles and checks exception called inside callback function. 554 | * Either exception class name or exception instance should be provided. 555 | * 556 | * ```php 557 | * expectException(MyException::class, function() { 559 | * $this->doSomethingBad(); 560 | * }); 561 | * 562 | * $I->expectException(new MyException(), function() { 563 | * $this->doSomethingBad(); 564 | * }); 565 | * ``` 566 | * If you want to check message or exception code, you can pass them with exception instance: 567 | * ```php 568 | * expectException(new MyException("Don't do bad things"), function() { 571 | * $this->doSomethingBad(); 572 | * }); 573 | * ``` 574 | * 575 | * @param $exception string or \Exception 576 | * @param $callback 577 | * 578 | * @deprecated Use expectThrowable instead 579 | * @see \Codeception\Module\Asserts::expectException() 580 | */ 581 | public function expectException($exception, $callback) 582 | { 583 | return $this->getScenario()->runStep(new Action('expectException', func_get_args())); 584 | } 585 | 586 | 587 | /** 588 | * [!] Method is generated. Documentation taken from corresponding module. 589 | * 590 | * Handles and checks throwables (Exceptions/Errors) called inside the callback function. 591 | * Either throwable class name or throwable instance should be provided. 592 | * 593 | * ```php 594 | * expectThrowable(MyThrowable::class, function() { 596 | * $this->doSomethingBad(); 597 | * }); 598 | * 599 | * $I->expectThrowable(new MyException(), function() { 600 | * $this->doSomethingBad(); 601 | * }); 602 | * ``` 603 | * If you want to check message or throwable code, you can pass them with throwable instance: 604 | * ```php 605 | * expectThrowable(new MyError("Don't do bad things"), function() { 608 | * $this->doSomethingBad(); 609 | * }); 610 | * ``` 611 | * 612 | * @param $throwable string or \Throwable 613 | * @param $callback 614 | * @see \Codeception\Module\Asserts::expectThrowable() 615 | */ 616 | public function expectThrowable($throwable, $callback) 617 | { 618 | return $this->getScenario()->runStep(new Action('expectThrowable', func_get_args())); 619 | } 620 | } 621 | -------------------------------------------------------------------------------- /tests/unit.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | # 3 | # Suite for unit (internal) tests. 4 | 5 | class_name: UnitTester 6 | modules: 7 | enabled: 8 | - Asserts 9 | - \Helper\Unit -------------------------------------------------------------------------------- /tests/unit/ConfigTest.php: -------------------------------------------------------------------------------- 1 | 'test', 25 | 'redirectUrl' => 'http://google.com', 26 | 'privateKeyPath' => '/tmp', 27 | 'certPath' => '/tmp', 28 | 'scope' => ['test', 'test2', 'test3'], 29 | ]); 30 | 31 | $this->assertSame('test test2 test3', $config->getScopeString()); 32 | } 33 | 34 | /** 35 | * Data provider for @see ConfigTest::testConstruct() 36 | * 37 | * @return array 38 | */ 39 | public function dataProviderForConstructor(): array 40 | { 41 | return [ 42 | 'min' => [ 43 | [ 44 | 'clientId' => 'test', 45 | 'redirectUrl' => 'http://google.com', 46 | 'privateKeyPath' => '/tmp', 47 | 'certPath' => '/tmp', 48 | 'scope' => ['test', 'test2', 'test3'], 49 | ], 50 | null, 51 | ], 52 | 'max' => [ 53 | [ 54 | 'clientId' => 'test', 55 | 'redirectUrl' => 'http://google.com', 56 | 'privateKeyPath' => '/tmp', 57 | 'certPath' => '/tmp', 58 | 'portalUrl' => 'google.com', 59 | 'tokenUrlPath' => 'test', 60 | 'codeUrlPath' => 'test', 61 | 'personUrlPath' => 'test', 62 | 'logoutUrlPath' => 'test', 63 | 'privateKeyPassword' => 'test', 64 | 'oid' => 'test', 65 | 'responseType' => 'test', 66 | 'accessType' => 'test', 67 | 'tmpPath' => 'test', 68 | 'token' => 'test', 69 | 'scope' => ['test', 'test2', 'test3'], 70 | ], 71 | null, 72 | ], 73 | 'No cert path' => [ 74 | [ 75 | 'clientId' => 'test', 76 | 'redirectUrl' => 'http://google.com', 77 | 'privateKeyPath' => '/tmp', 78 | 'scope' => ['test', 'test2', 'test3'], 79 | ], 80 | InvalidConfigurationException::class, 81 | ], 82 | 'No private key path' => [ 83 | [ 84 | 'clientId' => 'test', 85 | 'redirectUrl' => 'http://google.com', 86 | 'certPath' => '/tmp', 87 | 'scope' => ['test', 'test2', 'test3'], 88 | ], 89 | InvalidConfigurationException::class, 90 | ], 91 | 'No redirect url' => [ 92 | [ 93 | 'clientId' => 'test', 94 | 'privateKeyPath' => '/tmp', 95 | 'certPath' => '/tmp', 96 | 'scope' => ['test', 'test2', 'test3'], 97 | ], 98 | InvalidConfigurationException::class, 99 | ], 100 | 'No client id' => [ 101 | [ 102 | 'redirectUrl' => 'http://google.com', 103 | 'privateKeyPath' => '/tmp', 104 | 'certPath' => '/tmp', 105 | 'scope' => ['test', 'test2', 'test3'], 106 | ], 107 | InvalidConfigurationException::class, 108 | ], 109 | 'invalid scope' => [ 110 | [ 111 | 'redirectUrl' => 'http://google.com', 112 | 'privateKeyPath' => '/tmp', 113 | 'certPath' => '/tmp', 114 | 'scope' => 'test test2 test3', 115 | ], 116 | InvalidConfigurationException::class, 117 | ], 118 | ]; 119 | } 120 | 121 | /** 122 | * @param $config 123 | * @param string|null $expectedException 124 | * @throws \Esia\Exceptions\InvalidConfigurationException 125 | * 126 | * @dataProvider dataProviderForConstructor 127 | */ 128 | public function testConstruct($config, string $expectedException = null): void 129 | { 130 | if ($expectedException) { 131 | $this->expectException($expectedException); 132 | } 133 | 134 | new Config($config); 135 | } 136 | 137 | /** 138 | * @throws InvalidConfigurationException 139 | */ 140 | public function testGetTokenUrl(): void 141 | { 142 | $config = new Config([ 143 | 'clientId' => 'test', 144 | 'redirectUrl' => 'http://google.com', 145 | 'privateKeyPath' => '/tmp', 146 | 'certPath' => '/tmp', 147 | 'portalUrl' => 'https://google.com/', 148 | 'tokenUrlPath' => 'test', 149 | 'scope' => ['test', 'test2', 'test3'], 150 | ]); 151 | 152 | $this->assertSame('https://google.com/test', $config->getTokenUrl()); 153 | } 154 | 155 | /** 156 | * @throws InvalidConfigurationException 157 | */ 158 | public function testGetCodeUrl(): void 159 | { 160 | $config = new Config([ 161 | 'clientId' => 'test', 162 | 'redirectUrl' => 'http://google.com', 163 | 'privateKeyPath' => '/tmp', 164 | 'certPath' => '/tmp', 165 | 'portalUrl' => 'https://google.com/', 166 | 'codeUrlPath' => 'test', 167 | 'scope' => ['test', 'test2', 'test3'], 168 | ]); 169 | 170 | $this->assertSame('https://google.com/test', $config->getCodeUrl()); 171 | } 172 | 173 | /** 174 | * @throws InvalidConfigurationException 175 | */ 176 | public function testGetPersonUrl(): void 177 | { 178 | $config = new Config([ 179 | 'clientId' => 'test', 180 | 'redirectUrl' => 'http://google.com', 181 | 'privateKeyPath' => '/tmp', 182 | 'certPath' => '/tmp', 183 | 'portalUrl' => 'https://google.com/', 184 | 'personUrlPath' => 'test', 185 | 'oid' => 'test', 186 | 'scope' => ['test', 'test2', 'test3'], 187 | ]); 188 | 189 | $this->assertSame('https://google.com/test/test', $config->getPersonUrl()); 190 | } 191 | /** 192 | * @throws InvalidConfigurationException 193 | */ 194 | public function testGetPersonUrlWithoutOid(): void 195 | { 196 | $config = new Config([ 197 | 'clientId' => 'test', 198 | 'redirectUrl' => 'http://google.com', 199 | 'privateKeyPath' => '/tmp', 200 | 'certPath' => '/tmp', 201 | 'portalUrl' => 'https://google.com/', 202 | 'personUrlPath' => 'test', 203 | 'scope' => ['test', 'test2', 'test3'], 204 | ]); 205 | 206 | $this->expectException(InvalidConfigurationException::class); 207 | $this->assertSame('https://google.com/test/test', $config->getPersonUrl()); 208 | } 209 | /** 210 | * @throws InvalidConfigurationException 211 | */ 212 | public function testGetLogoutUrl(): void 213 | { 214 | $config = new Config([ 215 | 'clientId' => 'test', 216 | 'redirectUrl' => 'http://google.com', 217 | 'privateKeyPath' => '/tmp', 218 | 'certPath' => '/tmp', 219 | 'portalUrl' => 'https://google.com/', 220 | 'logoutUrlPath' => 'test', 221 | 'scope' => ['test', 'test2', 'test3'], 222 | ]); 223 | 224 | $this->assertSame('https://google.com/test', $config->getLogoutUrl()); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /tests/unit/Http/GuzzleHttpClientTest.php: -------------------------------------------------------------------------------- 1 | $handler]); 36 | 37 | $client = new GuzzleHttpClient($guzzleClient); 38 | 39 | $response = $client->sendRequest(new Request('GET', '/')); 40 | 41 | self::assertSame(200, $response->getStatusCode()); 42 | 43 | $this->expectException(ClientExceptionInterface::class); 44 | $client->sendRequest(new Request('GET', '/')); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/unit/OpenIdCliOpensslTest.php: -------------------------------------------------------------------------------- 1 | config = [ 20 | 'clientId' => 'INSP03211', 21 | 'redirectUrl' => 'http://my-site.com/response.php', 22 | 'portalUrl' => 'https://esia-portal1.test.gosuslugi.ru/', 23 | 'privateKeyPath' => codecept_data_dir('server-gost.key'), 24 | 'privateKeyPassword' => 'test', 25 | 'certPath' => codecept_data_dir('server-gost.crt'), 26 | 'tmpPath' => codecept_log_dir(), 27 | ]; 28 | 29 | $config = new Config($this->config); 30 | 31 | $this->openId = new OpenId($config); 32 | $this->openId->setSigner(new CliSignerPKCS7( 33 | $this->config['certPath'], 34 | $this->config['privateKeyPath'], 35 | $this->config['privateKeyPassword'], 36 | $this->config['tmpPath'] 37 | )); 38 | } 39 | 40 | /** 41 | * @throws AbstractEsiaException 42 | * @throws InvalidConfigurationException 43 | */ 44 | public function testGetToken(): void 45 | { 46 | $config = new Config($this->config); 47 | 48 | $oid = '123'; 49 | $oidBase64 = base64_encode('{ "urn:esia:sbj_id" : ' . $oid . '}'); 50 | 51 | $client = $this->buildClientWithResponses([ 52 | new Response(200, [], '{ "access_token": "test.' . $oidBase64 . '.test"}'), 53 | ]); 54 | $openId = new OpenId($config, $client); 55 | $openId->setSigner(new CliSignerPKCS7( 56 | $this->config['certPath'], 57 | $this->config['privateKeyPath'], 58 | $this->config['privateKeyPassword'], 59 | $this->config['tmpPath'] 60 | )); 61 | $token = $openId->getToken('test'); 62 | self::assertNotEmpty($token); 63 | self::assertSame($oid, $openId->getConfig()->getOid()); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /tests/unit/OpenIdTest.php: -------------------------------------------------------------------------------- 1 | config = [ 33 | 'clientId' => 'INSP03211', 34 | 'redirectUrl' => 'http://my-site.com/response.php', 35 | 'portalUrl' => 'https://esia-portal1.test.gosuslugi.ru/', 36 | 'privateKeyPath' => codecept_data_dir('server.key'), 37 | 'privateKeyPassword' => 'test', 38 | 'certPath' => codecept_data_dir('server.crt'), 39 | 'tmpPath' => codecept_log_dir(), 40 | ]; 41 | 42 | $config = new Config($this->config); 43 | 44 | $this->openId = new OpenId($config); 45 | } 46 | 47 | /** 48 | * @throws SignFailException 49 | * @throws AbstractEsiaException 50 | * @throws InvalidConfigurationException 51 | */ 52 | public function testGetToken(): void 53 | { 54 | $config = new Config($this->config); 55 | 56 | $oid = '123'; 57 | $oidBase64 = base64_encode('{ "urn:esia:sbj_id" : ' . $oid . '}'); 58 | 59 | $client = $this->buildClientWithResponses([ 60 | new Response(200, [], '{ "access_token": "test.' . $oidBase64 . '.test"}'), 61 | ]); 62 | $openId = new OpenId($config, $client); 63 | 64 | $token = $openId->getToken('test'); 65 | self::assertNotEmpty($token); 66 | self::assertSame($oid, $openId->getConfig()->getOid()); 67 | } 68 | 69 | /** 70 | * @throws InvalidConfigurationException 71 | * @throws AbstractEsiaException 72 | */ 73 | public function testGetPersonInfo(): void 74 | { 75 | $config = new Config($this->config); 76 | $oid = '123'; 77 | $config->setOid($oid); 78 | $config->setToken('test'); 79 | 80 | $client = $this->buildClientWithResponses([ 81 | new Response(200, [], '{"username": "test"}'), 82 | ]); 83 | $openId = new OpenId($config, $client); 84 | 85 | $info = $openId->getPersonInfo(); 86 | self::assertNotEmpty($info); 87 | self::assertSame(['username' => 'test'], $info); 88 | } 89 | 90 | /** 91 | * @throws InvalidConfigurationException 92 | * @throws AbstractEsiaException 93 | */ 94 | public function testGetContactInfo(): void 95 | { 96 | $config = new Config($this->config); 97 | $oid = '123'; 98 | $config->setOid($oid); 99 | $config->setToken('test'); 100 | 101 | $client = $this->buildClientWithResponses([ 102 | new Response(200, [], '{"size": 2, "elements": ["phone", "email"]}'), 103 | new Response(200, [], '{"phone": "555 555 555"}'), 104 | new Response(200, [], '{"email": "test@gmail.com"}'), 105 | ]); 106 | $openId = new OpenId($config, $client); 107 | 108 | $info = $openId->getContactInfo(); 109 | self::assertNotEmpty($info); 110 | self::assertSame([['phone' => '555 555 555'], ['email' => 'test@gmail.com']], $info); 111 | } 112 | 113 | /** 114 | * @throws InvalidConfigurationException 115 | * @throws AbstractEsiaException 116 | */ 117 | public function testGetAddressInfo(): void 118 | { 119 | $config = new Config($this->config); 120 | $oid = '123'; 121 | $config->setOid($oid); 122 | $config->setToken('test'); 123 | 124 | $client = $this->buildClientWithResponses([ 125 | new Response(200, [], '{"size": 2, "elements": ["phone", "email"]}'), 126 | new Response(200, [], '{"phone": "555 555 555"}'), 127 | new Response(200, [], '{"email": "test@gmail.com"}'), 128 | ]); 129 | $openId = new OpenId($config, $client); 130 | 131 | $info = $openId->getAddressInfo(); 132 | self::assertNotEmpty($info); 133 | self::assertSame([['phone' => '555 555 555'], ['email' => 'test@gmail.com']], $info); 134 | } 135 | 136 | /** 137 | * @throws InvalidConfigurationException 138 | * @throws AbstractEsiaException 139 | */ 140 | public function testGetDocInfo(): void 141 | { 142 | $config = new Config($this->config); 143 | $oid = '123'; 144 | $config->setOid($oid); 145 | $config->setToken('test'); 146 | 147 | $client = $this->buildClientWithResponses([ 148 | new Response(200, [], '{"size": 2, "elements": ["phone", "email"]}'), 149 | new Response(200, [], '{"phone": "555 555 555"}'), 150 | new Response(200, [], '{"email": "test@gmail.com"}'), 151 | ]); 152 | $openId = new OpenId($config, $client); 153 | 154 | $info = $openId->getDocInfo(); 155 | self::assertNotEmpty($info); 156 | self::assertSame([['phone' => '555 555 555'], ['email' => 'test@gmail.com']], $info); 157 | } 158 | 159 | /** 160 | * @throws InvalidConfigurationException 161 | */ 162 | public function testBuildLogoutUrl(): void 163 | { 164 | $config = $this->openId->getConfig(); 165 | 166 | $url = $config->getLogoutUrl() . '?client_id=' . $config->getClientId(); 167 | $logoutUrl = $this->openId->buildLogoutUrl(); 168 | self::assertSame($url, $logoutUrl); 169 | } 170 | 171 | /** 172 | * @throws InvalidConfigurationException 173 | */ 174 | public function testBuildLogoutUrlWithRedirect(): void 175 | { 176 | $config = $this->openId->getConfig(); 177 | 178 | $redirectUrl = 'test.example.com'; 179 | $url = $config->getLogoutUrl() . '?client_id=' . $config->getClientId() . '&redirect_url=' . $redirectUrl; 180 | $logoutUrl = $this->openId->buildLogoutUrl($redirectUrl); 181 | self::assertSame($url, $logoutUrl); 182 | } 183 | 184 | /** 185 | * Client with prepared responses 186 | * 187 | * @param array $responses 188 | * @return ClientInterface 189 | */ 190 | protected function buildClientWithResponses(array $responses): ClientInterface 191 | { 192 | $mock = new MockHandler($responses); 193 | 194 | $handler = HandlerStack::create($mock); 195 | $guzzleClient = new Client(['handler' => $handler]); 196 | 197 | return new GuzzleHttpClient($guzzleClient); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /tests/unit/Signer/SignerPKCS7Test.php: -------------------------------------------------------------------------------- 1 | sign('test'); 34 | self::assertNotEmpty($sign); 35 | } 36 | 37 | /** 38 | * @throws SignFailException 39 | */ 40 | public function testSignCertDoesNotExists(): void 41 | { 42 | $signer = new SignerPKCS7( 43 | '/test', 44 | codecept_data_dir('server.key'), 45 | 'test', 46 | codecept_log_dir() 47 | ); 48 | 49 | $this->expectException(NoSuchCertificateFileException::class); 50 | $signer->sign('test'); 51 | } 52 | 53 | /** 54 | * @throws SignFailException 55 | */ 56 | public function testPrivateKeyDoesNotExists(): void 57 | { 58 | $signer = new SignerPKCS7( 59 | codecept_data_dir('server.crt'), 60 | '/test', 61 | 'test', 62 | codecept_log_dir() 63 | ); 64 | 65 | $this->expectException(NoSuchKeyFileException::class); 66 | $signer->sign('test'); 67 | } 68 | 69 | /** 70 | * @throws SignFailException 71 | */ 72 | public function testTmpDirDoesNotExists(): void 73 | { 74 | $signer = new SignerPKCS7( 75 | codecept_data_dir('server.crt'), 76 | codecept_data_dir('server.key'), 77 | 'test', 78 | '/' 79 | ); 80 | 81 | $this->expectException(NoSuchTmpDirException::class); 82 | $signer->sign('test'); 83 | } 84 | 85 | /** 86 | * @throws SignFailException 87 | */ 88 | public function testTmpDirIsNotWritable(): void 89 | { 90 | $signer = new SignerPKCS7( 91 | codecept_data_dir('server.crt'), 92 | codecept_data_dir('server.key'), 93 | 'test', 94 | codecept_log_dir('non_writable_directory') 95 | ); 96 | 97 | $this->expectException(NoSuchTmpDirException::class); 98 | $signer->sign('test'); 99 | } 100 | 101 | /** 102 | * @throws SignFailException 103 | */ 104 | public function testCertificateIsNotReadable(): void 105 | { 106 | $signer = new SignerPKCS7( 107 | codecept_data_dir('non_readable_file'), 108 | codecept_data_dir('server.key'), 109 | 'test', 110 | codecept_log_dir() 111 | ); 112 | 113 | $this->expectException(CannotReadCertificateException::class); 114 | $signer->sign('test'); 115 | } 116 | 117 | /** 118 | * @throws SignFailException 119 | */ 120 | public function testPrivateKeyIsNotReadable(): void 121 | { 122 | $signer = new SignerPKCS7( 123 | codecept_data_dir('server.crt'), 124 | codecept_data_dir('non_readable_file'), 125 | 'test', 126 | codecept_log_dir() 127 | ); 128 | 129 | $this->expectException(CannotReadPrivateKeyException::class); 130 | $signer->sign('test'); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /tests/unit/_bootstrap.php: -------------------------------------------------------------------------------- 1 |