Configuration   A
last analyzed

Complexity

Total Complexity 14

Size/Duplication

Total Lines 272
Duplicated Lines 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 218
c 2
b 1
f 0
dl 0
loc 272
rs 10
wmc 14

4 Methods

Rating   Name   Duplication   Size   Complexity  
A extractorsNode() 0 18 1
B apiKeyNode() 0 75 1
A getConfigTreeBuilder() 0 19 1
C jwtNode() 0 140 11
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Damax\Bundle\ApiAuthBundle\DependencyInjection;
6
7
use Damax\Bundle\ApiAuthBundle\Security\JsonResponseFactory;
8
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
9
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
10
use Symfony\Component\Config\Definition\ConfigurationInterface;
11
12
final class Configuration implements ConfigurationInterface
13
{
14
    public const SIGNER_SYMMETRIC = 'symmetric';
15
    public const SIGNER_ASYMMETRIC = 'asymmetric';
16
17
    private const SYMMETRIC_ALGOS = ['HS256', 'HS384', 'HS512'];
18
    private const ASYMMETRIC_ALGOS = ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512'];
19
20
    public const STORAGE_FIXED = 'fixed';
21
    public const STORAGE_REDIS = 'redis';
22
    public const STORAGE_DOCTRINE = 'doctrine';
23
24
    public function getConfigTreeBuilder(): TreeBuilder
25
    {
26
        $treeBuilder = new TreeBuilder('damax_api_auth');
27
28
        /** @var ArrayNodeDefinition $rootNode */
29
        $rootNode = $treeBuilder->getRootNode();
30
        $rootNode
31
            ->addDefaultsIfNotSet()
32
            ->children()
33
                ->scalarNode('response_factory_service_id')
34
                    ->cannotBeEmpty()
35
                    ->defaultValue(JsonResponseFactory::class)
36
                ->end()
37
                ->append($this->apiKeyNode('api_key'))
38
                ->append($this->jwtNode('jwt'))
39
            ->end()
40
        ;
41
42
        return $treeBuilder;
43
    }
44
45
    private function apiKeyNode(string $name): ArrayNodeDefinition
46
    {
47
        return (new ArrayNodeDefinition($name))
48
            ->canBeEnabled()
49
            ->children()
50
                ->append($this->extractorsNode('extractors', [
51
                    [
52
                        'type' => 'header',
53
                        'name' => 'Authorization',
54
                        'prefix' => 'Token',
55
                    ],
56
                ]))
57
58
                ->arrayNode('generator')
59
                    ->addDefaultsIfNotSet()
60
                    ->children()
61
                        ->integerNode('key_size')
62
                            ->defaultValue(20)
63
                        ->end()
64
                    ->end()
65
                ->end()
66
67
                ->arrayNode('storage')
68
                    ->beforeNormalization()
69
                        ->ifTrue(function (array $config): bool {
70
                            return !isset($config[0]);
71
                        })
72
                        ->then(function (array $config): array {
73
                            return [
74
                                ['type' => self::STORAGE_FIXED, 'tokens' => $config],
75
                            ];
76
                        })
77
                    ->end()
78
                    ->arrayPrototype()
79
                        ->children()
80
                            ->enumNode('type')
81
                                ->isRequired()
82
                                ->values([self::STORAGE_FIXED, self::STORAGE_REDIS, self::STORAGE_DOCTRINE])
83
                            ->end()
84
                            ->arrayNode('tokens')
85
                                ->useAttributeAsKey(true)
86
                                ->requiresAtLeastOneElement()
87
                                ->scalarPrototype()
88
                                    ->isRequired()
89
                                ->end()
90
                            ->end()
91
                            ->booleanNode('writable')
92
                                ->defaultFalse()
93
                            ->end()
94
                            ->scalarNode('redis_client_id')
95
                                ->cannotBeEmpty()
96
                                ->defaultValue('snc_redis.default')
97
                            ->end()
98
                            ->scalarNode('key_prefix')
99
                                ->cannotBeEmpty()
100
                            ->end()
101
                            ->scalarNode('doctrine_connection_id')
102
                                ->cannotBeEmpty()
103
                                ->defaultValue('database_connection')
104
                            ->end()
105
                            ->scalarNode('table_name')
106
                                ->cannotBeEmpty()
107
                                ->defaultValue('api_key')
108
                            ->end()
109
                            ->arrayNode('fields')
110
                                ->children()
111
                                    ->scalarNode('key')->cannotBeEmpty()->end()
112
                                    ->scalarNode('ttl')->cannotBeEmpty()->end()
113
                                    ->scalarNode('identity')->cannotBeEmpty()->end()
114
                                ->end()
115
                            ->end()
116
                        ->end()
117
                    ->end()
118
                ->end()
119
            ->end()
120
        ;
121
    }
122
123
    private function jwtNode(string $name): ArrayNodeDefinition
124
    {
125
        return (new ArrayNodeDefinition($name))
126
            ->beforeNormalization()
127
                ->ifString()
128
                ->then(function (string $config): array {
129
                    return ['signer' => $config];
130
                })
131
            ->end()
132
            ->canBeEnabled()
133
            ->children()
134
                ->append($this->extractorsNode('extractors', [
135
                    [
136
                        'type' => 'header',
137
                        'name' => 'Authorization',
138
                        'prefix' => 'Bearer',
139
                    ],
140
                ]))
141
                ->scalarNode('identity_claim')
142
                    ->cannotBeEmpty()
143
                ->end()
144
                ->arrayNode('builder')
145
                    ->addDefaultsIfNotSet()
146
                    ->children()
147
                        ->scalarNode('issuer')
148
                            ->cannotBeEmpty()
149
                        ->end()
150
                        ->scalarNode('audience')
151
                            ->cannotBeEmpty()
152
                        ->end()
153
                        ->integerNode('ttl')
154
                            ->defaultValue(3600)
155
                        ->end()
156
                    ->end()
157
                ->end()
158
                ->arrayNode('parser')
159
                    ->children()
160
                        ->arrayNode('issuers')
161
                            ->requiresAtLeastOneElement()
162
                            ->scalarPrototype()
163
                                ->isRequired()
164
                            ->end()
165
                        ->end()
166
                        ->scalarNode('audience')
167
                            ->cannotBeEmpty()
168
                        ->end()
169
                    ->end()
170
                ->end()
171
                ->arrayNode('signer')
172
                    ->isRequired()
173
                    ->beforeNormalization()
174
                        ->ifString()
175
                        ->then(function (string $config): array {
176
                            return ['signing_key' => $config];
177
                        })
178
                    ->end()
179
                    ->beforeNormalization()
180
                        ->ifTrue(function (?array $config): bool {
181
                            $type = $config['type'] ?? self::SIGNER_SYMMETRIC;
182
183
                            return self::SIGNER_ASYMMETRIC === $type;
184
                        })
185
                        ->then(function (array $config): array {
186
                            if (isset($config['signing_key'])) {
187
                                $config['signing_key'] = 'file://' . $config['signing_key'];
188
                            }
189
190
                            if (isset($config['verification_key'])) {
191
                                $config['verification_key'] = 'file://' . $config['verification_key'];
192
                            }
193
194
                            if (!isset($config['algorithm'])) {
195
                                $config['algorithm'] = self::ASYMMETRIC_ALGOS[0];
196
                            }
197
198
                            return $config;
199
                        })
200
                    ->end()
201
                    ->validate()
202
                        ->ifTrue(function (array $config): bool {
203
                            return self::SIGNER_ASYMMETRIC === $config['type'] && empty($config['verification_key']);
204
                        })
205
                        ->thenInvalid('Verification key must be specified for "asymmetric" signer.')
206
                    ->end()
207
                    ->validate()
208
                        ->ifTrue(function (array $config): bool {
209
                            return self::SIGNER_SYMMETRIC === $config['type'] && !in_array($config['algorithm'], self::SYMMETRIC_ALGOS);
210
                        })
211
                        ->thenInvalid('HMAC algorithm must be specified for "symmetric" signer.')
212
                    ->end()
213
                    ->validate()
214
                        ->ifTrue(function (array $config): bool {
215
                            return self::SIGNER_ASYMMETRIC === $config['type'] && !in_array($config['algorithm'], self::ASYMMETRIC_ALGOS);
216
                        })
217
                        ->thenInvalid('RSA or ECDSA algorithm must be specified for "asymmetric" signer.')
218
                    ->end()
219
                    ->validate()
220
                        ->ifTrue(function (array $config): bool {
221
                            if (self::SIGNER_SYMMETRIC === $config['type']) {
222
                                return false;
223
                            }
224
225
                            return !is_readable($config['signing_key']) || !is_readable($config['verification_key']);
226
                        })
227
                        ->thenInvalid('Signing and/or verification key is not readable.')
228
                    ->end()
229
                    ->validate()
230
                        ->ifTrue(function (array $config): bool {
231
                            return self::SIGNER_SYMMETRIC === $config['type'] && !empty($config['verification_key']);
232
                        })
233
                        ->thenInvalid('Verification key must no be specified for "symmetric" signer.')
234
                    ->end()
235
                    ->validate()
236
                        ->ifTrue(function (array $config): bool {
237
                            return self::SIGNER_SYMMETRIC === $config['type'] && !empty($config['passphrase']);
238
                        })
239
                        ->thenInvalid('Passphrase must not be specified for "symmetric" signer.')
240
                    ->end()
241
                    ->children()
242
                        ->enumNode('type')
243
                            ->values(['symmetric', 'asymmetric'])
244
                            ->defaultValue('symmetric')
245
                        ->end()
246
                        ->enumNode('algorithm')
247
                            ->values(array_merge(self::SYMMETRIC_ALGOS, self::ASYMMETRIC_ALGOS))
248
                            ->defaultValue(self::SYMMETRIC_ALGOS[0])
249
                        ->end()
250
                        ->scalarNode('signing_key')
251
                            ->isRequired()
252
                        ->end()
253
                        ->scalarNode('verification_key')
254
                            ->cannotBeEmpty()
255
                        ->end()
256
                        ->scalarNode('passphrase')
257
                            ->cannotBeEmpty()
258
                            ->defaultValue('')
259
                        ->end()
260
                    ->end()
261
                ->end()
262
            ->end()
263
        ;
264
    }
265
266
    private function extractorsNode(string $name, array $defaults): ArrayNodeDefinition
267
    {
268
        return (new ArrayNodeDefinition($name))
269
            ->arrayPrototype()
270
                ->children()
271
                    ->enumNode('type')
272
                        ->isRequired()
273
                        ->values(['header', 'query', 'cookie'])
274
                    ->end()
275
                    ->scalarNode('name')
276
                        ->isRequired()
277
                    ->end()
278
                    ->scalarNode('prefix')
279
                        ->cannotBeEmpty()
280
                    ->end()
281
                ->end()
282
            ->end()
283
            ->defaultValue($defaults)
284
        ;
285
    }
286
}
287