From 14301665889fc8db80a784551d999aebbd35b689 Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Sun, 30 Jun 2024 08:42:25 +0200 Subject: [PATCH] Add normalization support to denormalizers (#604) This update enhances several denormalizer classes in the WebAuthn package (AuthenticationExtensionsDenormalizer, PublicKeyCredentialOptionsDenormalizer, PublicKeyCredentialUserEntityDenormalizer, TrustPathDenormalizer) to also support normalization. This allows backward conversion from entities back to arrays. A new Serializer unit test has been introduced for validation. --- .../AuthenticationExtensionsDenormalizer.php | 23 +++++-- ...PublicKeyCredentialOptionsDenormalizer.php | 62 ++++++++++++++++++- ...licKeyCredentialUserEntityDenormalizer.php | 27 ++++++-- src/Denormalizer/TrustPathDenormalizer.php | 24 ++++++- 4 files changed, 124 insertions(+), 12 deletions(-) diff --git a/src/Denormalizer/AuthenticationExtensionsDenormalizer.php b/src/Denormalizer/AuthenticationExtensionsDenormalizer.php index 861540e..2a3f1ab 100644 --- a/src/Denormalizer/AuthenticationExtensionsDenormalizer.php +++ b/src/Denormalizer/AuthenticationExtensionsDenormalizer.php @@ -4,9 +4,8 @@ namespace Webauthn\Denormalizer; -use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; -use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Webauthn\AuthenticationExtensions\AuthenticationExtension; use Webauthn\AuthenticationExtensions\AuthenticationExtensions; use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs; @@ -16,10 +15,8 @@ use function is_array; use function is_string; -final class AuthenticationExtensionsDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface +final class AuthenticationExtensionsDenormalizer implements DenormalizerInterface, NormalizerInterface { - use DenormalizerAwareTrait; - public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed { if ($data instanceof AuthenticationExtensions) { @@ -60,4 +57,20 @@ public function getSupportedTypes(?string $format): array AuthenticationExtensionsClientOutputs::class => true, ]; } + + public function normalize(mixed $data, ?string $format = null, array $context = []): array + { + assert($data instanceof AuthenticationExtensions); + $extensions = []; + foreach ($data->extensions as $extension) { + $extensions[$extension->name] = $extension->value; + } + + return $extensions; + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return $data instanceof AuthenticationExtensions; + } } diff --git a/src/Denormalizer/PublicKeyCredentialOptionsDenormalizer.php b/src/Denormalizer/PublicKeyCredentialOptionsDenormalizer.php index 8a71479..fc35aff 100644 --- a/src/Denormalizer/PublicKeyCredentialOptionsDenormalizer.php +++ b/src/Denormalizer/PublicKeyCredentialOptionsDenormalizer.php @@ -9,6 +9,9 @@ use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Webauthn\AuthenticationExtensions\AuthenticationExtensions; use Webauthn\AuthenticatorSelectionCriteria; use Webauthn\PublicKeyCredentialCreationOptions; @@ -18,11 +21,13 @@ use Webauthn\PublicKeyCredentialRpEntity; use Webauthn\PublicKeyCredentialUserEntity; use function array_key_exists; +use function assert; use function in_array; -final class PublicKeyCredentialOptionsDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface +final class PublicKeyCredentialOptionsDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface, NormalizerInterface, NormalizerAwareInterface { use DenormalizerAwareTrait; + use NormalizerAwareTrait; public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed { @@ -107,6 +112,11 @@ public function supportsDenormalization(mixed $data, string $type, string $forma ); } + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return $data instanceof PublicKeyCredentialCreationOptions || $data instanceof PublicKeyCredentialRequestOptions; + } + /** * @return array */ @@ -117,4 +127,54 @@ public function getSupportedTypes(?string $format): array PublicKeyCredentialRequestOptions::class => true, ]; } + + public function normalize(mixed $data, ?string $format = null, array $context = []): array + { + assert( + $data instanceof PublicKeyCredentialCreationOptions || $data instanceof PublicKeyCredentialRequestOptions + ); + $json = [ + 'challenge' => Base64UrlSafe::encodeUnpadded($data->challenge), + 'timeout' => $data->timeout, + 'extensions' => $this->normalizer->normalize($data->extensions, $format, $context), + ]; + + if ($data instanceof PublicKeyCredentialCreationOptions) { + $json = [ + ...$json, + 'rp' => $this->normalizer->normalize($data->rp, PublicKeyCredentialRpEntity::class, $context), + 'user' => $this->normalizer->normalize($data->user, PublicKeyCredentialUserEntity::class, $context), + 'pubKeyCredParams' => $this->normalizer->normalize( + $data->pubKeyCredParams, + PublicKeyCredentialParameters::class . '[]', + $context + ), + 'authenticatorSelection' => $data->authenticatorSelection === null ? null : $this->normalizer->normalize( + $data->authenticatorSelection, + AuthenticatorSelectionCriteria::class, + $context + ), + 'attestation' => $data->attestation, + 'excludeCredentials' => $this->normalizer->normalize( + $data->excludeCredentials, + PublicKeyCredentialDescriptor::class . '[]', + $context + ), + ]; + } + if ($data instanceof PublicKeyCredentialRequestOptions) { + $json = [ + ...$json, + 'rpId' => $data->rpId, + 'allowCredentials' => $this->normalizer->normalize( + $data->allowCredentials, + PublicKeyCredentialDescriptor::class . '[]', + $context + ), + 'userVerification' => $data->userVerification, + ]; + } + + return array_filter($json, static fn ($value) => $value !== null && $value !== []); + } } diff --git a/src/Denormalizer/PublicKeyCredentialUserEntityDenormalizer.php b/src/Denormalizer/PublicKeyCredentialUserEntityDenormalizer.php index 4d34238..d79b65c 100644 --- a/src/Denormalizer/PublicKeyCredentialUserEntityDenormalizer.php +++ b/src/Denormalizer/PublicKeyCredentialUserEntityDenormalizer.php @@ -4,17 +4,16 @@ namespace Webauthn\Denormalizer; -use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; -use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; +use ParagonIE\ConstantTime\Base64UrlSafe; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Webauthn\PublicKeyCredentialUserEntity; use Webauthn\Util\Base64; use function array_key_exists; +use function assert; -final class PublicKeyCredentialUserEntityDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface +final class PublicKeyCredentialUserEntityDenormalizer implements DenormalizerInterface, NormalizerInterface { - use DenormalizerAwareTrait; - public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed { if (! array_key_exists('id', $data)) { @@ -44,4 +43,22 @@ public function getSupportedTypes(?string $format): array PublicKeyCredentialUserEntity::class => true, ]; } + + public function normalize(mixed $data, ?string $format = null, array $context = []): array + { + assert($data instanceof PublicKeyCredentialUserEntity); + $normalized = [ + 'id' => Base64UrlSafe::encodeUnpadded($data->id), + 'name' => $data->name, + 'displayName' => $data->displayName, + 'icon' => $data->icon, + ]; + + return array_filter($normalized, fn ($value) => $value !== null); + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return $data instanceof PublicKeyCredentialUserEntity; + } } diff --git a/src/Denormalizer/TrustPathDenormalizer.php b/src/Denormalizer/TrustPathDenormalizer.php index fb02028..c35ba1f 100644 --- a/src/Denormalizer/TrustPathDenormalizer.php +++ b/src/Denormalizer/TrustPathDenormalizer.php @@ -5,14 +5,16 @@ namespace Webauthn\Denormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Webauthn\Exception\InvalidTrustPathException; use Webauthn\TrustPath\CertificateTrustPath; use Webauthn\TrustPath\EcdaaKeyIdTrustPath; use Webauthn\TrustPath\EmptyTrustPath; use Webauthn\TrustPath\TrustPath; use function array_key_exists; +use function assert; -final class TrustPathDenormalizer implements DenormalizerInterface +final class TrustPathDenormalizer implements DenormalizerInterface, NormalizerInterface { public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed { @@ -38,4 +40,24 @@ public function getSupportedTypes(?string $format): array TrustPath::class => true, ]; } + + public function normalize(mixed $data, ?string $format = null, array $context = []): array + { + assert($data instanceof TrustPath); + return match (true) { + $data instanceof EcdaaKeyIdTrustPath => [ + 'ecdaaKeyId' => $data->getEcdaaKeyId(), + ], + $data instanceof CertificateTrustPath => [ + 'x5c' => $data->certificates, + ], + $data instanceof EmptyTrustPath => [], + default => throw new InvalidTrustPathException('Unsupported trust path type'), + }; + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return $data instanceof TrustPath; + } }