Skip to content

Commit

Permalink
Merge pull request #476 from web-auth/property-access
Browse files Browse the repository at this point in the history
MDS Denormalizer
  • Loading branch information
Spomky committed Sep 17, 2023
2 parents 526ffe6 + 4bf45ec commit fbdcf73
Show file tree
Hide file tree
Showing 116 changed files with 2,370 additions and 436 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
"php-http/curl-client": "^2.2",
"php-http/mock-client": "^1.5",
"php-parallel-lint/php-parallel-lint": "^1.3",
"phpdocumentor/reflection-docblock": "^5.3",
"phpstan/extension-installer": "^1.3",
"phpstan/phpstan": "^1.8",
"phpstan/phpstan-deprecation-rules": "^1.0",
Expand Down
500 changes: 442 additions & 58 deletions phpstan-baseline.neon

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/metadata-service/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
}
},
"suggest": {
"symfony/serializer": "As of 4.5.x, the symfony/serializer component will become mandatory for converting objects such as the Metadata Statement",
"phpdocumentor/reflection-docblock": "As of 4.5.x, the phpdocumentor/reflection-docblock component will become mandatory for converting objects such as the Metadata Statement",
"psr/clock-implementation": "As of 4.5.x, the PSR Clock implementation will replace lcobucci/clock",
"psr/log-implementation": "Recommended to receive logs from the library",
"web-token/jwt-key-mgmt": "Mandatory for fetching Metadata Statement from distant sources",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public static function fixPEMStructure(string $data, string $type = 'CERTIFICATE

/**
* @deprecated since 4.7.0 and will be removed in 5.0.0. No replacement as not used internally.
* @infection-ignore-all
*/
public static function convertPEMToDER(string $data): string
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace Webauthn\MetadataService\Denormalizer;

use Symfony\Component\Serializer\Exception\BadMethodCallException;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Webauthn\MetadataService\Statement\ExtensionDescriptor;
use function array_key_exists;

final class ExtensionDescriptorDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface
{
use DenormalizerAwareTrait;

private const ALREADY_CALLED = 'EXTENSION_DESCRIPTOR_PREPROCESS_ALREADY_CALLED';

public function denormalize(mixed $data, string $type, string $format = null, array $context = [])
{
if ($this->denormalizer === null) {
throw new BadMethodCallException('Please set a denormalizer before calling denormalize()!');
}

if (array_key_exists('fail_if_unknown', $data)) {
$data['failIfUnknown'] = $data['fail_if_unknown'];
unset($data['fail_if_unknown']);
}

$context[self::ALREADY_CALLED] = true;

return $this->denormalizer->denormalize($data, $type, $format, $context);
}

public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
{
if ($context[self::ALREADY_CALLED] ?? false) {
return false;
}

return $type === ExtensionDescriptor::class;
}

/**
* @return array<class-string, bool>
*/
public function getSupportedTypes(?string $format): array
{
return [
ExtensionDescriptor::class => false,
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types=1);

namespace Webauthn\MetadataService\Denormalizer;

use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\UidNormalizer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\SerializerInterface;

final class MetadataStatementSerializerFactory
{
public static function create(): ?SerializerInterface
{
foreach (self::getRequiredSerializerClasses() as $class => $package) {
if (! class_exists($class)) {
return null;
}
}

$denormalizers = [
new ExtensionDescriptorDenormalizer(),
new UidNormalizer(),
new ArrayDenormalizer(),
new ObjectNormalizer(
propertyTypeExtractor: new PropertyInfoExtractor(typeExtractors: [
new PhpDocExtractor(),
new ReflectionExtractor(),
])
),
];

return new Serializer($denormalizers, [new JsonEncoder()]);
}

/**
* @return array<class-string, string>
*/
private static function getRequiredSerializerClasses(): array
{
return [
UidNormalizer::class => 'symfony/serializer',
ArrayDenormalizer::class => 'symfony/serializer',
ObjectNormalizer::class => 'symfony/serializer',
PropertyInfoExtractor::class => 'symfony/serializer',
PhpDocExtractor::class => 'phpdocumentor/reflection-docblock',
ReflectionExtractor::class => 'symfony/serializer',
JsonEncoder::class => 'symfony/serializer',
Serializer::class => 'symfony/serializer',
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Webauthn\MetadataService\Denormalizer\MetadataStatementSerializerFactory;
use Webauthn\MetadataService\Event\CanDispatchEvents;
use Webauthn\MetadataService\Event\MetadataStatementFound;
use Webauthn\MetadataService\Event\NullEventDispatcher;
Expand All @@ -23,6 +25,8 @@ final class DistantResourceMetadataService implements MetadataService, CanDispat

private EventDispatcherInterface $dispatcher;

private readonly ?SerializerInterface $serializer;

/**
* @param array<string, string> $additionalHeaderParameters
*/
Expand All @@ -32,6 +36,7 @@ public function __construct(
private readonly string $uri,
private readonly bool $isBase64Encoded = false,
private readonly array $additionalHeaderParameters = [],
?SerializerInterface $serializer = null,
) {
if ($requestFactory !== null && ! $httpClient instanceof HttpClientInterface) {
trigger_deprecation(
Expand All @@ -40,6 +45,7 @@ public function __construct(
'The parameter "$requestFactory" will be removed in 5.0.0. Please set it to null and set an Symfony\Contracts\HttpClient\HttpClientInterface as "$httpClient" argument.'
);
}
$this->serializer = $serializer ?? MetadataStatementSerializerFactory::create();
$this->dispatcher = new NullEventDispatcher();
}

Expand All @@ -56,9 +62,10 @@ public static function create(
ClientInterface|HttpClientInterface $httpClient,
string $uri,
bool $isBase64Encoded = false,
array $additionalHeaderParameters = []
array $additionalHeaderParameters = [],
?SerializerInterface $serializer = null
): self {
return new self($requestFactory, $httpClient, $uri, $isBase64Encoded, $additionalHeaderParameters);
return new self($requestFactory, $httpClient, $uri, $isBase64Encoded, $additionalHeaderParameters, $serializer);
}

public function list(): iterable
Expand Down Expand Up @@ -105,6 +112,11 @@ private function loadData(): void
if ($this->isBase64Encoded) {
$content = Base64::decode($content, true);
}
if ($this->serializer !== null) {
$this->statement = $this->serializer->deserialize($content, MetadataStatement::class, 'json');
return;
}

$this->statement = MetadataStatement::createFromString($content);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Throwable;
use Webauthn\MetadataService\CertificateChain\CertificateChainValidator;
use Webauthn\MetadataService\CertificateChain\CertificateToolbox;
use Webauthn\MetadataService\Denormalizer\MetadataStatementSerializerFactory;
use Webauthn\MetadataService\Event\CanDispatchEvents;
use Webauthn\MetadataService\Event\MetadataStatementFound;
use Webauthn\MetadataService\Event\NullEventDispatcher;
Expand Down Expand Up @@ -45,6 +47,8 @@ final class FidoAllianceCompliantMetadataService implements MetadataService, Can

private EventDispatcherInterface $dispatcher;

private readonly ?SerializerInterface $serializer;

/**
* @param array<string, mixed> $additionalHeaderParameters
*/
Expand All @@ -55,6 +59,7 @@ public function __construct(
private readonly array $additionalHeaderParameters = [],
private readonly ?CertificateChainValidator $certificateChainValidator = null,
private readonly ?string $rootCertificateUri = null,
?SerializerInterface $serializer = null,
) {
if ($requestFactory !== null && ! $httpClient instanceof HttpClientInterface) {
trigger_deprecation(
Expand All @@ -63,6 +68,7 @@ public function __construct(
'The parameter "$requestFactory" will be removed in 5.0.0. Please set it to null and set an Symfony\Contracts\HttpClient\HttpClientInterface as "$httpClient" argument.'
);
}
$this->serializer = $serializer ?? MetadataStatementSerializerFactory::create();
$this->dispatcher = new NullEventDispatcher();
}

Expand All @@ -81,14 +87,16 @@ public static function create(
array $additionalHeaderParameters = [],
?CertificateChainValidator $certificateChainValidator = null,
?string $rootCertificateUri = null,
?SerializerInterface $serializer = null,
): self {
return new self(
$requestFactory,
$httpClient,
$uri,
$additionalHeaderParameters,
$certificateChainValidator,
$rootCertificateUri
$rootCertificateUri,
$serializer,
);
}

Expand Down Expand Up @@ -139,8 +147,20 @@ private function loadData(): void
$jwtCertificates = [];
try {
$payload = $this->getJwsPayload($content, $jwtCertificates);
$data = json_decode($payload, true, flags: JSON_THROW_ON_ERROR);
$this->validateCertificates(...$jwtCertificates);
if ($this->serializer !== null) {
$blob = $this->serializer->deserialize($payload, MetadataBLOBPayload::class, 'json');
foreach ($blob->entries as $entry) {
$mds = $entry->metadataStatement;
if ($mds !== null && $entry->aaguid !== null) {
$this->statements[$entry->aaguid] = $mds;
$this->statusReports[$entry->aaguid] = $entry->statusReports;
}
}
$this->loaded = true;
return;
}
$data = json_decode($payload, true, flags: JSON_THROW_ON_ERROR);

foreach ($data['entries'] as $datum) {
$entry = MetadataBLOBPayloadEntry::createFromArray($datum);
Expand Down
23 changes: 19 additions & 4 deletions src/metadata-service/src/Service/FolderResourceMetadataService.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
namespace Webauthn\MetadataService\Service;

use InvalidArgumentException;
use Symfony\Component\Serializer\SerializerInterface;
use Webauthn\MetadataService\Denormalizer\MetadataStatementSerializerFactory;
use Webauthn\MetadataService\Exception\MetadataStatementLoadingException;
use Webauthn\MetadataService\Statement\MetadataStatement;
use function file_get_contents;
Expand All @@ -14,17 +16,25 @@

final class FolderResourceMetadataService implements MetadataService
{
private readonly string $rootPath;
private readonly ?SerializerInterface $serializer;

public function __construct(string $rootPath)
{
public function __construct(
private string $rootPath,
?SerializerInterface $serializer = null,
) {
$this->serializer = $serializer ?? MetadataStatementSerializerFactory::create();
$this->rootPath = rtrim($rootPath, DIRECTORY_SEPARATOR);
is_dir($this->rootPath) || throw new InvalidArgumentException('The given parameter is not a valid folder.');
is_readable($this->rootPath) || throw new InvalidArgumentException(
'The given parameter is not a valid folder.'
);
}

public static function create(string $rootPath, ?SerializerInterface $serializer = null): self
{
return new self($rootPath, $serializer);
}

public function list(): iterable
{
$files = glob($this->rootPath . DIRECTORY_SEPARATOR . '*');
Expand Down Expand Up @@ -53,7 +63,12 @@ public function get(string $aaguid): MetadataStatement
));
$filename = $this->rootPath . DIRECTORY_SEPARATOR . $aaguid;
$data = trim(file_get_contents($filename));
$mds = MetadataStatement::createFromString($data);
if ($this->serializer !== null) {
$mds = $this->serializer->deserialize($data, MetadataStatement::class, 'json');
} else {
$mds = MetadataStatement::createFromString($data);
}

$mds->aaguid !== null || throw MetadataStatementLoadingException::create('Invalid Metadata Statement.');

return $mds;
Expand Down
Loading

0 comments on commit fbdcf73

Please sign in to comment.