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: