Passed
Pull Request — master (#7)
by Pol
22:18 queued 20:04
created

ApiGwKeyLoader   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 185
Duplicated Lines 0 %

Test Coverage

Coverage 98.51%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 22
eloc 86
c 3
b 0
f 0
dl 0
loc 185
ccs 66
cts 67
cp 0.9851
rs 10

10 Methods

Rating   Name   Duplication   Size   Complexity  
A getFailsafePrivateKey() 0 3 1
A getPassphrase() 0 4 1
A loadKey() 0 27 5
A getFailsafePublicKey() 0 3 1
A findFirstFileExist() 0 17 3
A getEnvironment() 0 19 4
A getPublicKey() 0 3 1
A getSigningKey() 0 3 1
A loadFailsafeKey() 0 24 4
A __construct() 0 12 1
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
8
declare(strict_types=1);
9
10
namespace EcPhp\ApiGwAuthenticationBundle\Service\KeyLoader;
11
12
use EcPhp\ApiGwAuthenticationBundle\Exception\ApiGwAuthenticationException;
13
use EcPhp\ApiGwAuthenticationBundle\Service\KeyConverter\KeyConverterInterface;
14
use Lexik\Bundle\JWTAuthenticationBundle\Services\KeyLoader\RawKeyLoader;
15
use Psr\Http\Client\ClientInterface;
16
use Psr\Http\Message\RequestFactoryInterface;
17
use Throwable;
18
19
use function array_key_exists;
20
21
/**
22
 * Class ApiGwKeyLoader.
23
 *
24
 * phpcs:disable Generic.Files.LineLength.TooLong
25
 */
26
final class ApiGwKeyLoader implements KeyLoaderInterface
27
{
28
    private const API_GW_ACCEPTANCE = 'https://api.acceptance.tech.ec.europa.eu/federation/oauth/token/.well-known/jwks.json';
29
30
    private const API_GW_INTRA = 'https://intrapi.tech.ec.europa.eu/federation/oauth/token/.well-known/jwks.json';
31
32
    private const API_GW_PRODUCTION = 'https://api.tech.ec.europa.eu/federation/oauth/token/.well-known/jwks.json';
33
34
    private const LOCAL_FAILSAFE_PATH = '/../../Resources/keys';
35
36
    private array $environment;
37
38
    private ClientInterface $httpClient;
39
40
    private KeyConverterInterface $keyConverter;
41
42
    private static array $mapping = [
43
        [
44
            'env' => 'production',
45
            KeyLoaderInterface::TYPE_PUBLIC => self::API_GW_PRODUCTION,
46
            KeyLoaderInterface::TYPE_PRIVATE => '',
47
            'failsafe' => [
48
                KeyLoaderInterface::TYPE_PUBLIC => self::LOCAL_FAILSAFE_PATH . '/production/jwks.json',
49
                KeyLoaderInterface::TYPE_PRIVATE => '',
50
            ],
51
        ],
52
        [
53
            'env' => 'intra',
54
            KeyLoaderInterface::TYPE_PUBLIC => self::API_GW_INTRA,
55
            KeyLoaderInterface::TYPE_PRIVATE => '',
56
            'failsafe' => [
57
                KeyLoaderInterface::TYPE_PUBLIC => self::LOCAL_FAILSAFE_PATH . '/intra/jwks.json',
58
                KeyLoaderInterface::TYPE_PRIVATE => '',
59
            ],
60
        ],
61
        [
62
            'env' => 'acceptance',
63
            KeyLoaderInterface::TYPE_PUBLIC => self::API_GW_ACCEPTANCE,
64
            KeyLoaderInterface::TYPE_PRIVATE => '',
65
            'failsafe' => [
66
                KeyLoaderInterface::TYPE_PUBLIC => self::LOCAL_FAILSAFE_PATH . '/acceptance/jwks.json',
67
                KeyLoaderInterface::TYPE_PRIVATE => '',
68
            ],
69
        ],
70
    ];
71
72
    private string $projectDir;
73
74
    private RequestFactoryInterface $requestFactory;
75
76 10
    public function __construct(
77
        ClientInterface $httpClient,
78
        RequestFactoryInterface $requestFactory,
79
        KeyConverterInterface $keyConverter,
80
        string $projectDir,
81
        array $configuration
82
    ) {
83 10
        $this->httpClient = $httpClient;
84 10
        $this->requestFactory = $requestFactory;
85 10
        $this->keyConverter = $keyConverter;
86 10
        $this->projectDir = $projectDir;
87 10
        $this->environment = $this->getEnvironment($configuration['defaults']['env'], $configuration['envs']);
88 10
    }
89
90 9
    public function getPassphrase(): string
91
    {
92
        // Todo: Not supported yet.
93 9
        return '';
94
    }
95
96 9
    public function getPublicKey(): string
97
    {
98 9
        return $this->environment[KeyLoaderInterface::TYPE_PUBLIC] ?? '';
99
    }
100
101 9
    public function getSigningKey(): string
102
    {
103 9
        return $this->environment[KeyLoaderInterface::TYPE_PRIVATE] ?? '';
104
    }
105
106 9
    public function loadKey($type): string
107
    {
108 9
        $publicKey = $this->getPublicKey();
109 9
        $signingKey = $this->getSigningKey();
110 9
        $passPhrase = $this->getPassphrase();
111
112 9
        $key = KeyLoaderInterface::TYPE_PUBLIC === $type ? $publicKey : $signingKey;
113
114 9
        if ('user' === $this->environment['env']) {
115 6
            $keyPathCandidateParts = $this->findFirstFileExist($key);
116
117 6
            if ([] !== $keyPathCandidateParts) {
118 3
                $prefix = current($keyPathCandidateParts);
119
120 3
                return (new RawKeyLoader($prefix . $signingKey, $prefix . $publicKey, $passPhrase))
121 3
                    ->loadKey($type);
122
            }
123
        }
124
125
        try {
126 6
            $key = (new JWKSKeyLoader($this, $this->httpClient, $this->requestFactory, $this->keyConverter))
127 6
                ->loadKey($type);
128 6
        } catch (Throwable $e) {
129 6
            $key = $this->loadFailsafeKey($type);
130
        }
131
132 4
        return $key;
133
    }
134
135 9
    private function findFirstFileExist(string $key): array
136
    {
137 9
        $candidates = array_map(
138 9
            static fn (string $directory): array => [$directory, $key],
139
            [
140 9
                $this->projectDir,
141
                __DIR__,
142
            ]
143
        );
144
145 9
        foreach ($candidates as $candidate) {
146 9
            if (true === file_exists(implode('', $candidate))) {
147 9
                return $candidate;
148
            }
149
        }
150
151 3
        return [];
152
    }
153
154 10
    private function getEnvironment(string $env, array $configuredEnvs): array
155
    {
156 10
        $envs = [];
157
158 10
        foreach ($configuredEnvs as $name => $data) {
159 8
            $envs[] = [
160 8
                'env' => $name,
161 8
                KeyLoaderInterface::TYPE_PUBLIC => $data[KeyLoaderInterface::TYPE_PUBLIC],
162 8
                KeyLoaderInterface::TYPE_PRIVATE => $data[KeyLoaderInterface::TYPE_PRIVATE],
163
                'failsafe' => [
164 8
                    KeyLoaderInterface::TYPE_PUBLIC => $data['failsafe'][KeyLoaderInterface::TYPE_PUBLIC] ?? $data[KeyLoaderInterface::TYPE_PUBLIC],
165 8
                    KeyLoaderInterface::TYPE_PRIVATE => $data['failsafe'][KeyLoaderInterface::TYPE_PRIVATE] ?? $data[KeyLoaderInterface::TYPE_PRIVATE],
166
                ],
167
            ];
168
        }
169
170 10
        foreach (array_merge(self::$mapping, $envs) as $mapping) {
171 10
            if ($mapping['env'] === $env) {
172 10
                return $mapping;
173
            }
174
        }
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return array. Consider adding a return statement or allowing null as return value.

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:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
175
    }
176
177 1
    private function getFailsafePrivateKey(): string
178
    {
179 1
        return $this->environment['failsafe'][KeyLoaderInterface::TYPE_PRIVATE];
180
    }
181
182 5
    private function getFailsafePublicKey(): string
183
    {
184 5
        return $this->environment['failsafe'][KeyLoaderInterface::TYPE_PUBLIC];
185
    }
186
187 6
    private function loadFailsafeKey(string $type): string
188
    {
189 6
        $key = KeyLoaderInterface::TYPE_PUBLIC === $type ?
190 5
            $this->getFailsafePublicKey() :
191 6
            $this->getFailsafePrivateKey();
192
193 6
        $keyPathCandidateParts = $this->findFirstFileExist($key);
194
195
        // Todo: Remove duplicated code in here and JWKSKeyLoader.
196 6
        $jwksArray = json_decode(file_get_contents(implode('', $keyPathCandidateParts)), true);
197
198 6
        if (false === array_key_exists('keys', $jwksArray)) {
199 1
            throw new ApiGwAuthenticationException(
200 1
                sprintf('Invalid JWKS format of %s key at %s.', $type, $key)
201
            );
202
        }
203
204 5
        if ([] === $jwksArray['keys']) {
205 1
            throw new ApiGwAuthenticationException(
206 1
                sprintf('Invalid JWKS format of %s key at %s, keys array is empty.', $type, $key)
207
            );
208
        }
209
210 4
        return current($this->keyConverter->fromJWKStoPEMS($jwksArray['keys']));
211
    }
212
}
213