Test Failed
Push — master ( 9eb68a...5271f3 )
by Thomas
02:48
created

ServerBuilder::setupExtensions()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 9
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 14
ccs 0
cts 0
cp 0
crap 12
rs 9.9666
1
<?php
2
3
namespace MadWizard\WebAuthn\Builder;
4
5
use Closure;
6
use GuzzleHttp\Client;
7
use MadWizard\WebAuthn\Attestation\AttestationType;
8
use MadWizard\WebAuthn\Attestation\Registry\AttestationFormatRegistry;
9
use MadWizard\WebAuthn\Attestation\Registry\AttestationFormatRegistryInterface;
10
use MadWizard\WebAuthn\Attestation\TrustAnchor\TrustPathValidator;
11
use MadWizard\WebAuthn\Attestation\TrustAnchor\TrustPathValidatorInterface;
12
use MadWizard\WebAuthn\Attestation\Verifier\AndroidKeyAttestationVerifier;
13
use MadWizard\WebAuthn\Attestation\Verifier\AndroidSafetyNetAttestationVerifier;
14
use MadWizard\WebAuthn\Attestation\Verifier\FidoU2fAttestationVerifier;
15
use MadWizard\WebAuthn\Attestation\Verifier\NoneAttestationVerifier;
16
use MadWizard\WebAuthn\Attestation\Verifier\PackedAttestationVerifier;
17
use MadWizard\WebAuthn\Attestation\Verifier\TpmAttestationVerifier;
18
use MadWizard\WebAuthn\Cache\CacheProviderInterface;
19
use MadWizard\WebAuthn\Cache\FileCacheProvider;
20
use MadWizard\WebAuthn\Config\RelyingParty;
21
use MadWizard\WebAuthn\Config\RelyingPartyInterface;
22
use MadWizard\WebAuthn\Credential\CredentialStoreInterface;
23
use MadWizard\WebAuthn\Exception\ConfigurationException;
24
use MadWizard\WebAuthn\Exception\UnsupportedException;
25
use MadWizard\WebAuthn\Extension\AppId\AppIdExtension;
26
use MadWizard\WebAuthn\Extension\ExtensionInterface;
27
use MadWizard\WebAuthn\Extension\ExtensionRegistry;
28
use MadWizard\WebAuthn\Extension\ExtensionRegistryInterface;
29
use MadWizard\WebAuthn\Metadata\MetadataResolver;
30
use MadWizard\WebAuthn\Metadata\MetadataResolverInterface;
31
use MadWizard\WebAuthn\Metadata\NullMetadataResolver;
32
use MadWizard\WebAuthn\Metadata\Provider\FileProvider;
33
use MadWizard\WebAuthn\Metadata\Provider\MetadataServiceProvider;
34
use MadWizard\WebAuthn\Metadata\Source\MetadataServiceSource;
35
use MadWizard\WebAuthn\Metadata\Source\MetadataSourceInterface;
36
use MadWizard\WebAuthn\Metadata\Source\StatementDirectorySource;
37
use MadWizard\WebAuthn\Pki\ChainValidator;
38
use MadWizard\WebAuthn\Pki\ChainValidatorInterface;
39
use MadWizard\WebAuthn\Policy\Policy;
40
use MadWizard\WebAuthn\Policy\PolicyInterface;
41
use MadWizard\WebAuthn\Policy\Trust\TrustDecisionManager;
42
use MadWizard\WebAuthn\Policy\Trust\TrustDecisionManagerInterface;
43
use MadWizard\WebAuthn\Policy\Trust\Voter\AllowEmptyMetadataVoter;
44
use MadWizard\WebAuthn\Policy\Trust\Voter\SupportedAttestationTypeVoter;
45
use MadWizard\WebAuthn\Policy\Trust\Voter\TrustAttestationTypeVoter;
46
use MadWizard\WebAuthn\Policy\Trust\Voter\TrustChainVoter;
47
use MadWizard\WebAuthn\Policy\Trust\Voter\UndesiredStatusReportVoter;
48
use MadWizard\WebAuthn\Remote\CachingClientFactory;
49
use MadWizard\WebAuthn\Remote\Downloader;
50
use MadWizard\WebAuthn\Remote\DownloaderInterface;
51
use MadWizard\WebAuthn\Server\ServerInterface;
52
use MadWizard\WebAuthn\Server\WebAuthnServer;
53
use Psr\Log\LoggerAwareInterface;
54
use Psr\Log\LoggerInterface;
55
use Psr\Log\NullLogger;
56
57
final class ServerBuilder
58
{
59
    /**
60
     * @var RelyingParty|null
61
     */
62
    private $rp;
63
64
    /**
65
     * @var CredentialStoreInterface|null
66
     */
67
    private $store;
68
69
    /**
70
     * @var string|null;
71
     */
72
    private $cacheDir;
73
74
    /**
75
     * @var callable|PolicyCallbackInterface|null
76
     */
77
    private $policyCallback;
78
79
    /**
80
     * @var MetadataSourceInterface[]
81
     */
82
    private $metadataSources = [];
83
84
    /**
85
     * @var LoggerInterface|null
86
     */
87
    private $logger;
88
89
    /**
90
     * @var bool
91
     */
92
    private $allowNoneAttestation = true;
93
94
    /**
95
     * @var bool
96
     */
97
    private $allowSelfAttestation = true;
98
99
    /**
100
     * @var bool
101
     */
102
    private $trustWithoutMetadata = true;
103
104
    /**
105
     * @var bool
106
     */
107
    private $useMetadata = true;
108
109
    /**
110 18
     * @var bool
111
     */
112 18
    private $strictSupportedFormats = false;
113
114 18
    /**
115
     * @var string[]
116 18
     */
117 18
    private $enabledExtensions = [];
118
119
    /**
120 18
     * @var ExtensionInterface[]
121
     */
122 18
    private $customExtensions = [];
123 18
124
    private const SUPPORTED_EXTENSIONS = [
125
        'appid' => AppIdExtension::class,
126
    ];
127
128
    public function __construct()
129
    {
130
    }
131
132
    public function setRelyingParty(RelyingParty $rp): self
133
    {
134
        $this->rp = $rp;
135
        return $this;
136
    }
137
138
    public function setCredentialStore(CredentialStoreInterface $store): self
139
    {
140
        $this->store = $store;
141
        return $this;
142
    }
143
144
    public function setCacheDirectory(string $directory): self
145
    {
146
        $this->cacheDir = $directory;
147
        return $this;
148
    }
149
150
    public function useSystemTempCache(string $subDirectory = 'webauthn-server-cache'): self
151
    {
152
        $this->cacheDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $subDirectory;
153
        return $this;
154
    }
155
156
    /**
157
     * @param callable|PolicyCallbackInterface $policyCallback
158
     *
159
     * @return $this
160
     */
161
    public function configurePolicy(callable $policyCallback): self
162
    {
163
        $this->policyCallback = $policyCallback;
164
        return $this;
165
    }
166
167
    /**
168
     * @return $this
169
     */
170
    public function allowNoneAttestation(bool $allow): self
171
    {
172
        $this->allowNoneAttestation = $allow;
173
        return $this;
174
    }
175
176
    /**
177
     * @return $this
178
     */
179
    public function strictSupportedFormats(bool $strict): self
180
    {
181
        $this->strictSupportedFormats = $strict;
182
        return $this;
183
    }
184
185
    /**
186
     * @return $this
187
     */
188
    public function useMetadata(bool $use): self
189
    {
190
        $this->useMetadata = $use;
191
        return $this;
192
    }
193
194
    /**
195
     * @return $this
196
     */
197
    public function allowSelfAttestation(bool $allow): self
198
    {
199
        $this->allowSelfAttestation = $allow;
200
        return $this;
201
    }
202
203
    /**
204
     * @return $this
205
     */
206
    public function trustWithoutMetadata(bool $trust): self
207
    {
208
        $this->trustWithoutMetadata = $trust;
209
        return $this;
210 18
    }
211
212 18
    /**
213
     * @return $this
214 18
     */
215
    public function enableExtensions(string ...$extensions): self
216
    {
217 18
        foreach ($extensions as $ext) {
218
            if (!isset(self::SUPPORTED_EXTENSIONS[$ext])) {
219 18
                throw new ConfigurationException(sprintf('Extension %s is not supported.', $ext));
220
            }
221 18
        }
222 18
        $this->enabledExtensions = array_merge($this->enabledExtensions, $extensions);
223 18
        return $this;
224
    }
225
226 18
    /**
227
     * @return $this
228
     */
229
    public function addCustomExtension(ExtensionInterface $extension): self
230
    {
231
        $this->customExtensions[] = $extension;
232 18
        return $this;
233
    }
234
235
    /**
236
     * @return $this
237
     */
238
    public function setLogger(LoggerInterface $logger): self
239
    {
240 18
        $this->logger = $logger;
241 18
        return $this;
242 18
    }
243
244 18
    private function assignLogger(LoggerAwareInterface $service): void
245
    {
246
        if ($this->logger !== null) {
247
            $service->setLogger($this->logger);
248
        }
249
    }
250
251
    public function build(): ServerInterface
252
    {
253
        $c = $this->setupContainer();
254
255
        return $c[ServerInterface::class];
256
    }
257
258
    private function setupContainer(): ServiceContainer
259
    {
260
        $c = new ServiceContainer();
261
262
        $this->setupConfiguredServices($c);
263
        $this->setupFormats($c);
264
        $this->setupTrustDecisionManager($c);
265
        $this->setupExtensions($c);
266
267
        $c[TrustPathValidatorInterface::class] = static function (ServiceContainer $c): TrustPathValidatorInterface {
268
            return new TrustPathValidator($c[ChainValidatorInterface::class]);
269
        };
270
271
        $c[ChainValidatorInterface::class] = static function (ServiceContainer $c): ChainValidatorInterface {
0 ignored issues
show
Unused Code introduced by
The parameter $c is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

271
        $c[ChainValidatorInterface::class] = static function (/** @scrutinizer ignore-unused */ ServiceContainer $c): ChainValidatorInterface {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
272
            // TODO
273
            //return new ChainValidator($c[CertificateStatusResolverInterface::class]);
274
            return new ChainValidator(null);
275
        };
276
277 18
        // TODO
278
//        $c[CertificateStatusResolverInterface::class] = static function (ServiceContainer $c) {
279 18
//            return new CertificateStatusResolver($c[DownloaderInterface::class], $c[CacheProviderInterface::class]);
280
//        };
281
282
        $c[PolicyInterface::class] = Closure::fromCallable([$this, 'createPolicy']);
283
        $c[MetadataResolverInterface::class] = Closure::fromCallable([$this, 'createMetadataResolver']);
284
        $c[ServerInterface::class] = Closure::fromCallable([$this, 'createServer']);
285 18
286
        return $c;
287
    }
288
289
    private function setupDownloader(ServiceContainer $c)
290
    {
291 18
        $this->setupCache($c);
292
        if (isset($c[DownloaderInterface::class])) {
293 18
            return;
294
        }
295 18
        $c[DownloaderInterface::class] = static function (ServiceContainer $c): DownloaderInterface {
296
            return new Downloader($c[Client::class]);
297 18
        };
298
        $c[Client::class] = static function (ServiceContainer $c): Client {
299
            $factory = new CachingClientFactory($c[CacheProviderInterface::class]);
300
            return $factory->createClient();
301 18
        };
302
    }
303
304 18
    private function setupCache(ServiceContainer $c)
305
    {
306 18
        if (isset($c[CacheProviderInterface::class])) {
307 18
            return;
308 18
        }
309 18
310 18
        $cacheDir = $this->cacheDir;
311 18
        if ($cacheDir === null) {
312 18
            throw new ConfigurationException('No cache directory configured. Use useCacheDirectory or useSystemTempCache.');
313
        }
314
        $c[CacheProviderInterface::class] = static function (ServiceContainer $c) use ($cacheDir): CacheProviderInterface {
0 ignored issues
show
Unused Code introduced by
The parameter $c is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

314
        $c[CacheProviderInterface::class] = static function (/** @scrutinizer ignore-unused */ ServiceContainer $c) use ($cacheDir): CacheProviderInterface {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
315
            return new FileCacheProvider($cacheDir);
316
        };
317
    }
318
319
    private function setupConfiguredServices(ServiceContainer $c): void
320
    {
321 18
        if ($this->rp === null) {
322
            throw new ConfigurationException('Relying party not configured. Use setRelyingParty.');
323 18
        }
324 18
325
        $c[RelyingPartyInterface::class] = function (): RelyingPartyInterface { return $this->rp; };
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->rp could return the type null which is incompatible with the type-hinted return MadWizard\WebAuthn\Config\RelyingPartyInterface. Consider adding an additional type-check to rule them out.
Loading history...
326
327
        if ($this->store === null) {
328
            throw new ConfigurationException('Credential store not configured. Use setCredentialStore.');
329 18
        }
330
331
        $c[CredentialStoreInterface::class] = function (): CredentialStoreInterface { return $this->store; };
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->store could return the type null which is incompatible with the type-hinted return MadWizard\WebAuthn\Crede...redentialStoreInterface. Consider adding an additional type-check to rule them out.
Loading history...
332 18
        $c[LoggerInterface::class] = function (): LoggerInterface { return $this->logger ?? new NullLogger(); };
333
    }
334 18
335 18
    private function createPolicy(ServiceContainer $c): PolicyInterface
0 ignored issues
show
Unused Code introduced by
The parameter $c is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

335
    private function createPolicy(/** @scrutinizer ignore-unused */ ServiceContainer $c): PolicyInterface

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
336
    {
337 18
        $policy = new Policy();
338 18
339
        if ($this->policyCallback !== null) {
340 18
            ($this->policyCallback)($policy);
341 18
        }
342
343 18
        return $policy;
344 18
    }
345 18
346 18
    private function createServer(ServiceContainer $c): ServerInterface
347
    {
348 18
        return new WebAuthnServer(
349
            $c[RelyingPartyInterface::class],
350 18
            $c[PolicyInterface::class],
351
            $c[CredentialStoreInterface::class],
352
            $c[AttestationFormatRegistryInterface::class],
353
            $c[MetadataResolverInterface::class],
354
            $c[TrustDecisionManagerInterface::class],
355
            $c[ExtensionRegistryInterface::class]);
356
    }
357
358
    public function addMetadataSource(MetadataSourceInterface $metadataSource): self
359
    {
360
        $this->metadataSources[] = $metadataSource;
361
        return $this;
362
    }
363
364
    private function createMetadataResolver(ServiceContainer $c): MetadataResolverInterface
365
    {
366
        if (count($this->metadataSources) === 0) {
367
            return new NullMetadataResolver();
368
        }
369
        return new MetadataResolver($this->createMetadataProviders($c));
370
    }
371
372
    private function setupTrustDecisionManager(ServiceContainer $c)
373 18
    {
374
        $c[TrustDecisionManagerInterface::class] = function (ServiceContainer $c): TrustDecisionManagerInterface {
375
            $tdm = new TrustDecisionManager();
376 18
377
            if ($this->allowNoneAttestation) {
378
                $tdm->addVoter(new TrustAttestationTypeVoter(AttestationType::NONE));
379 18
            }
380
            if ($this->allowSelfAttestation) {
381
                $tdm->addVoter(new TrustAttestationTypeVoter(AttestationType::SELF));
382 18
            }
383
            if ($this->trustWithoutMetadata) {
384
                $tdm->addVoter(new AllowEmptyMetadataVoter());
385 18
            }
386
            if ($this->useMetadata) {
387
                $tdm->addVoter(new SupportedAttestationTypeVoter());
388 18
                $tdm->addVoter(new UndesiredStatusReportVoter());
389
                $tdm->addVoter(new TrustChainVoter($c[TrustPathValidatorInterface::class]));
390
            }
391 18
            return $tdm;
392
        };
393
    }
394
395 18
    private function createMetadataProviders(ServiceContainer $c): array
396
    {
397 18
        $providers = [];
398
        foreach ($this->metadataSources as $source) {
399 18
            if ($source instanceof StatementDirectorySource) {
400 18
                $provider = new FileProvider($source);
401 18
            } elseif ($source instanceof MetadataServiceSource) {
402 18
                $this->setupDownloader($c);
403 18
                $provider = new MetadataServiceProvider($source, $c[DownloaderInterface::class], $c[CacheProviderInterface::class], $c[ChainValidatorInterface::class]);
404 18
            } else {
405
                throw new UnsupportedException(sprintf('No provider available for metadata source of type %s.', get_class($source)));
406 18
            }
407
408 18
            if ($provider instanceof LoggerAwareInterface) {
409
                $this->assignLogger($provider);
410
            }
411
            $providers[] = $provider;
412
        }
413
        return $providers;
414
    }
415
416
    private function setupFormats(ServiceContainer $c)
417
    {
418
        $c[PackedAttestationVerifier::class] = static function (): PackedAttestationVerifier {
419
            return new PackedAttestationVerifier();
420
        };
421
        $c[FidoU2fAttestationVerifier::class] = static function (): FidoU2fAttestationVerifier {
422
            return new FidoU2fAttestationVerifier();
423
        };
424
        $c[NoneAttestationVerifier::class] = static function (): NoneAttestationVerifier {
425
            return new NoneAttestationVerifier();
426
        };
427
        $c[TpmAttestationVerifier::class] = static function (): TpmAttestationVerifier {
428
            return new TpmAttestationVerifier();
429
        };
430
        $c[AndroidSafetyNetAttestationVerifier::class] = static function (): AndroidSafetyNetAttestationVerifier {
431
            return new AndroidSafetyNetAttestationVerifier();
432
        };
433
        $c[AndroidKeyAttestationVerifier::class] = static function (): AndroidKeyAttestationVerifier {
434
            return new AndroidKeyAttestationVerifier();
435
        };
436
437
        $c[AttestationFormatRegistryInterface::class] = function (ServiceContainer $c): AttestationFormatRegistryInterface {
438
            $registry = new AttestationFormatRegistry();
439
440
            $registry->strictSupportedFormats($this->strictSupportedFormats);
441
442
            $registry->addFormat($c[PackedAttestationVerifier::class]->getSupportedFormat());
443
            $registry->addFormat($c[FidoU2fAttestationVerifier::class]->getSupportedFormat());
444
            $registry->addFormat($c[NoneAttestationVerifier::class]->getSupportedFormat());
445
            $registry->addFormat($c[TpmAttestationVerifier::class]->getSupportedFormat());
446
            $registry->addFormat($c[AndroidSafetyNetAttestationVerifier::class]->getSupportedFormat());
447
            $registry->addFormat($c[AndroidKeyAttestationVerifier::class]->getSupportedFormat());
448
449
            return $registry;
450
        };
451
    }
452
453
    private function setupExtensions(ServiceContainer $c): void
454
    {
455
        $c[AppIdExtension::class] = static function (ServiceContainer $c): AppIdExtension {
0 ignored issues
show
Unused Code introduced by
The parameter $c is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

455
        $c[AppIdExtension::class] = static function (/** @scrutinizer ignore-unused */ ServiceContainer $c): AppIdExtension {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
456
            return new AppIdExtension();
457
        };
458
        $c[ExtensionRegistryInterface::class] = function (ServiceContainer $c): ExtensionRegistryInterface {
459
            $registry = new ExtensionRegistry();
460
            foreach (array_unique($this->enabledExtensions) as $ext) {
461
                $registry->addExtension($c[self::SUPPORTED_EXTENSIONS[$ext]]);
462
            }
463
            foreach ($this->customExtensions as $ext) {
464
                $registry->addExtension($ext);
465
            }
466
            return $registry;
467
        };
468
    }
469
}
470