| 1 | <?php |
||
| 2 | |||
| 3 | /** |
||
| 4 | * For the full copyright and license information, please view |
||
| 5 | * the LICENSE file that was distributed with this source code. |
||
| 6 | * |
||
| 7 | * @see https://github.com/ecphp |
||
| 8 | */ |
||
| 9 | |||
| 10 | declare(strict_types=1); |
||
| 11 | |||
| 12 | namespace EcPhp\ApiGwAuthenticationBundle\Service\KeyLoader; |
||
| 13 | |||
| 14 | use EcPhp\ApiGwAuthenticationBundle\Exception\ApiGwAuthenticationException; |
||
| 15 | use EcPhp\ApiGwAuthenticationBundle\Service\KeyConverter\KeyConverterInterface; |
||
| 16 | use Lexik\Bundle\JWTAuthenticationBundle\Services\KeyLoader\RawKeyLoader; |
||
| 17 | use Psr\Http\Client\ClientInterface; |
||
| 18 | use Psr\Http\Message\RequestFactoryInterface; |
||
| 19 | use Throwable; |
||
| 20 | |||
| 21 | use function array_key_exists; |
||
| 22 | |||
| 23 | /** |
||
| 24 | * Class ApiGwKeyLoader. |
||
| 25 | * |
||
| 26 | * phpcs:disable Generic.Files.LineLength.TooLong |
||
| 27 | */ |
||
| 28 | final class ApiGwKeyLoader implements KeyLoaderInterface |
||
| 29 | { |
||
| 30 | private const API_GW_ACCEPTANCE = 'https://api.acceptance.tech.ec.europa.eu/federation/oauth/token/.well-known/jwks.json'; |
||
| 31 | |||
| 32 | private const API_GW_INTRA = 'https://intrapi.tech.ec.europa.eu/federation/oauth/token/.well-known/jwks.json'; |
||
| 33 | |||
| 34 | private const API_GW_PRODUCTION = 'https://api.tech.ec.europa.eu/federation/oauth/token/.well-known/jwks.json'; |
||
| 35 | |||
| 36 | private const LOCAL_FAILSAFE_PATH = '/../../Resources/keys'; |
||
| 37 | |||
| 38 | private array $environment; |
||
| 39 | |||
| 40 | private ClientInterface $httpClient; |
||
| 41 | |||
| 42 | private KeyConverterInterface $keyConverter; |
||
| 43 | |||
| 44 | private static array $mapping = [ |
||
| 45 | [ |
||
| 46 | 'env' => 'production', |
||
| 47 | KeyLoaderInterface::TYPE_PUBLIC => self::API_GW_PRODUCTION, |
||
| 48 | KeyLoaderInterface::TYPE_PRIVATE => '', |
||
| 49 | 'failsafe' => [ |
||
| 50 | KeyLoaderInterface::TYPE_PUBLIC => self::LOCAL_FAILSAFE_PATH . '/production/jwks.json', |
||
| 51 | KeyLoaderInterface::TYPE_PRIVATE => '', |
||
| 52 | ], |
||
| 53 | ], |
||
| 54 | [ |
||
| 55 | 'env' => 'intra', |
||
| 56 | KeyLoaderInterface::TYPE_PUBLIC => self::API_GW_INTRA, |
||
| 57 | KeyLoaderInterface::TYPE_PRIVATE => '', |
||
| 58 | 'failsafe' => [ |
||
| 59 | KeyLoaderInterface::TYPE_PUBLIC => self::LOCAL_FAILSAFE_PATH . '/intra/jwks.json', |
||
| 60 | KeyLoaderInterface::TYPE_PRIVATE => '', |
||
| 61 | ], |
||
| 62 | ], |
||
| 63 | [ |
||
| 64 | 'env' => 'acceptance', |
||
| 65 | KeyLoaderInterface::TYPE_PUBLIC => self::API_GW_ACCEPTANCE, |
||
| 66 | KeyLoaderInterface::TYPE_PRIVATE => '', |
||
| 67 | 'failsafe' => [ |
||
| 68 | KeyLoaderInterface::TYPE_PUBLIC => self::LOCAL_FAILSAFE_PATH . '/acceptance/jwks.json', |
||
| 69 | KeyLoaderInterface::TYPE_PRIVATE => '', |
||
| 70 | ], |
||
| 71 | ], |
||
| 72 | ]; |
||
| 73 | |||
| 74 | private string $projectDir; |
||
| 75 | |||
| 76 | private RequestFactoryInterface $requestFactory; |
||
| 77 | |||
| 78 | 10 | public function __construct( |
|
| 79 | ClientInterface $httpClient, |
||
| 80 | RequestFactoryInterface $requestFactory, |
||
| 81 | KeyConverterInterface $keyConverter, |
||
| 82 | string $projectDir, |
||
| 83 | array $configuration |
||
| 84 | ) { |
||
| 85 | 10 | $this->httpClient = $httpClient; |
|
| 86 | 10 | $this->requestFactory = $requestFactory; |
|
| 87 | 10 | $this->keyConverter = $keyConverter; |
|
| 88 | 10 | $this->projectDir = $projectDir; |
|
| 89 | 10 | $this->environment = $this->getEnvironment($configuration['defaults']['env'], $configuration['envs']); |
|
| 90 | } |
||
| 91 | |||
| 92 | 9 | public function getPassphrase(): string |
|
| 93 | { |
||
| 94 | // Todo: Not supported yet. |
||
| 95 | 9 | return ''; |
|
| 96 | } |
||
| 97 | |||
| 98 | 9 | public function getPublicKey(): string |
|
| 99 | { |
||
| 100 | 9 | return $this->environment[KeyLoaderInterface::TYPE_PUBLIC] ?? ''; |
|
| 101 | } |
||
| 102 | |||
| 103 | 9 | public function getSigningKey(): string |
|
| 104 | { |
||
| 105 | 9 | return $this->environment[KeyLoaderInterface::TYPE_PRIVATE] ?? ''; |
|
| 106 | } |
||
| 107 | |||
| 108 | 9 | public function loadKey($type): string |
|
| 109 | { |
||
| 110 | 9 | $publicKey = $this->getPublicKey(); |
|
| 111 | 9 | $signingKey = $this->getSigningKey(); |
|
| 112 | 9 | $passPhrase = $this->getPassphrase(); |
|
| 113 | |||
| 114 | 9 | $key = KeyLoaderInterface::TYPE_PUBLIC === $type ? $publicKey : $signingKey; |
|
| 115 | |||
| 116 | 9 | if ('user' === $this->environment['env']) { |
|
| 117 | 6 | $keyPathCandidateParts = $this->findFirstFileExist($key); |
|
| 118 | |||
| 119 | 6 | if ([] !== $keyPathCandidateParts) { |
|
| 120 | 3 | $prefix = current($keyPathCandidateParts); |
|
| 121 | |||
| 122 | 3 | return (new RawKeyLoader($prefix . $signingKey, $prefix . $publicKey, $passPhrase)) |
|
| 123 | 3 | ->loadKey($type); |
|
| 124 | } |
||
| 125 | } |
||
| 126 | |||
| 127 | try { |
||
| 128 | 6 | $key = (new JWKSKeyLoader($this, $this->httpClient, $this->requestFactory, $this->keyConverter)) |
|
| 129 | 6 | ->loadKey($type); |
|
| 130 | 6 | } catch (Throwable $e) { |
|
| 131 | 6 | $key = $this->loadFailsafeKey($type); |
|
| 132 | } |
||
| 133 | |||
| 134 | 4 | return $key; |
|
| 135 | } |
||
| 136 | |||
| 137 | 9 | private function findFirstFileExist(string $key): array |
|
| 138 | { |
||
| 139 | 9 | $candidates = array_map( |
|
| 140 | 9 | static fn (string $directory): array => [$directory, $key], |
|
| 141 | [ |
||
| 142 | 9 | $this->projectDir, |
|
| 143 | __DIR__, |
||
| 144 | ] |
||
| 145 | ); |
||
| 146 | |||
| 147 | 9 | foreach ($candidates as $candidate) { |
|
| 148 | 9 | if (true === file_exists(implode('', $candidate))) { |
|
| 149 | 9 | return $candidate; |
|
| 150 | } |
||
| 151 | } |
||
| 152 | |||
| 153 | 3 | return []; |
|
| 154 | } |
||
| 155 | |||
| 156 | 10 | private function getEnvironment(string $env, array $configuredEnvs): array |
|
| 157 | { |
||
| 158 | 10 | $envs = []; |
|
| 159 | |||
| 160 | 10 | foreach ($configuredEnvs as $name => $data) { |
|
| 161 | 8 | $envs[] = [ |
|
| 162 | 'env' => $name, |
||
| 163 | 8 | KeyLoaderInterface::TYPE_PUBLIC => $data[KeyLoaderInterface::TYPE_PUBLIC], |
|
| 164 | 8 | KeyLoaderInterface::TYPE_PRIVATE => $data[KeyLoaderInterface::TYPE_PRIVATE], |
|
| 165 | 'failsafe' => [ |
||
| 166 | 8 | KeyLoaderInterface::TYPE_PUBLIC => $data['failsafe'][KeyLoaderInterface::TYPE_PUBLIC] ?? $data[KeyLoaderInterface::TYPE_PUBLIC], |
|
| 167 | 8 | KeyLoaderInterface::TYPE_PRIVATE => $data['failsafe'][KeyLoaderInterface::TYPE_PRIVATE] ?? $data[KeyLoaderInterface::TYPE_PRIVATE], |
|
| 168 | ], |
||
| 169 | ]; |
||
| 170 | } |
||
| 171 | |||
| 172 | 10 | foreach (array_merge(self::$mapping, $envs) as $mapping) { |
|
| 173 | 10 | if ($mapping['env'] === $env) { |
|
| 174 | 10 | return $mapping; |
|
| 175 | } |
||
| 176 | } |
||
|
0 ignored issues
–
show
|
|||
| 177 | } |
||
| 178 | |||
| 179 | 1 | private function getFailsafePrivateKey(): string |
|
| 180 | { |
||
| 181 | 1 | return $this->environment['failsafe'][KeyLoaderInterface::TYPE_PRIVATE]; |
|
| 182 | } |
||
| 183 | |||
| 184 | 5 | private function getFailsafePublicKey(): string |
|
| 185 | { |
||
| 186 | 5 | return $this->environment['failsafe'][KeyLoaderInterface::TYPE_PUBLIC]; |
|
| 187 | } |
||
| 188 | |||
| 189 | 6 | private function loadFailsafeKey(string $type): string |
|
| 190 | { |
||
| 191 | 6 | $key = KeyLoaderInterface::TYPE_PUBLIC === $type ? |
|
| 192 | 5 | $this->getFailsafePublicKey() : |
|
| 193 | 6 | $this->getFailsafePrivateKey(); |
|
| 194 | |||
| 195 | 6 | $keyPathCandidateParts = $this->findFirstFileExist($key); |
|
| 196 | |||
| 197 | // Todo: Remove duplicated code in here and JWKSKeyLoader. |
||
| 198 | 6 | $jwksArray = json_decode(file_get_contents(implode('', $keyPathCandidateParts)), true); |
|
| 199 | |||
| 200 | 6 | if (false === array_key_exists('keys', $jwksArray)) { |
|
| 201 | 1 | throw new ApiGwAuthenticationException( |
|
| 202 | 1 | sprintf('Invalid JWKS format of %s key at %s.', $type, $key) |
|
| 203 | ); |
||
| 204 | } |
||
| 205 | |||
| 206 | 5 | if ([] === $jwksArray['keys']) { |
|
| 207 | 1 | throw new ApiGwAuthenticationException( |
|
| 208 | 1 | sprintf('Invalid JWKS format of %s key at %s, keys array is empty.', $type, $key) |
|
| 209 | ); |
||
| 210 | } |
||
| 211 | |||
| 212 | 4 | return current($this->keyConverter->fromJWKStoPEMS($jwksArray['keys'])); |
|
| 213 | } |
||
| 214 | } |
||
| 215 |
For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example: