Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .ci-tools/phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -4266,6 +4266,12 @@ parameters:
count: 2
path: ../src/webauthn/src/Denormalizer/PublicKeyCredentialOptionsDenormalizer.php

-
rawMessage: Cannot access offset 'hints' on mixed.
identifier: offsetAccess.nonOffsetAccessible
count: 2
path: ../src/webauthn/src/Denormalizer/PublicKeyCredentialOptionsDenormalizer.php

-
rawMessage: Cannot access offset 'id' on mixed.
identifier: offsetAccess.nonOffsetAccessible
Expand Down Expand Up @@ -4386,6 +4392,12 @@ parameters:
count: 2
path: ../src/webauthn/src/Denormalizer/PublicKeyCredentialOptionsDenormalizer.php

-
rawMessage: 'Parameter #10 $hints of static method Webauthn\PublicKeyCredentialCreationOptions::create() expects array<string>, mixed given.'
identifier: argument.type
count: 1
path: ../src/webauthn/src/Denormalizer/PublicKeyCredentialOptionsDenormalizer.php

-
rawMessage: 'Parameter #2 $array of function array_key_exists expects array, mixed given.'
identifier: argument.type
Expand Down Expand Up @@ -4422,6 +4434,12 @@ parameters:
count: 1
path: ../src/webauthn/src/Denormalizer/PublicKeyCredentialOptionsDenormalizer.php

-
rawMessage: 'Parameter #7 $hints of static method Webauthn\PublicKeyCredentialRequestOptions::create() expects array<string>, mixed given.'
identifier: argument.type
count: 1
path: ../src/webauthn/src/Denormalizer/PublicKeyCredentialOptionsDenormalizer.php

-
rawMessage: 'Parameter #8 $timeout of static method Webauthn\PublicKeyCredentialCreationOptions::create() expects int<1, max>|null, mixed given.'
identifier: argument.type
Expand Down Expand Up @@ -7359,6 +7377,12 @@ parameters:
count: 1
path: ../src/webauthn/src/PublicKeyCredentialCreationOptions.php

-
rawMessage: Constructor in Webauthn\PublicKeyCredentialCreationOptions has parameter $hints with default value.
identifier: ergebnis.noConstructorParameterWithDefaultValue
count: 1
path: ../src/webauthn/src/PublicKeyCredentialCreationOptions.php

-
rawMessage: Constructor in Webauthn\PublicKeyCredentialCreationOptions has parameter $pubKeyCredParams with default value.
identifier: ergebnis.noConstructorParameterWithDefaultValue
Expand Down Expand Up @@ -7521,6 +7545,12 @@ parameters:
count: 1
path: ../src/webauthn/src/PublicKeyCredentialOptions.php

-
rawMessage: Constructor in Webauthn\PublicKeyCredentialOptions has parameter $hints with default value.
identifier: ergebnis.noConstructorParameterWithDefaultValue
count: 1
path: ../src/webauthn/src/PublicKeyCredentialOptions.php

-
rawMessage: Constructor in Webauthn\PublicKeyCredentialOptions has parameter $timeout with default value.
identifier: ergebnis.noConstructorParameterWithDefaultValue
Expand Down Expand Up @@ -7581,6 +7611,12 @@ parameters:
count: 1
path: ../src/webauthn/src/PublicKeyCredentialRequestOptions.php

-
rawMessage: Constructor in Webauthn\PublicKeyCredentialRequestOptions has parameter $hints with default value.
identifier: ergebnis.noConstructorParameterWithDefaultValue
count: 1
path: ../src/webauthn/src/PublicKeyCredentialRequestOptions.php

-
rawMessage: Constructor in Webauthn\PublicKeyCredentialRequestOptions has parameter $rpId with default value.
identifier: ergebnis.noConstructorParameterWithDefaultValue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use Webauthn\PublicKeyCredentialUserEntity;
use function array_key_exists;
use function assert;
use function count;
use function in_array;

final class PublicKeyCredentialOptionsDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface, NormalizerInterface, NormalizerAwareInterface
Expand Down Expand Up @@ -78,6 +79,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a
$format,
$context
),
$data['hints'] ?? [],
);
}
if ($type === PublicKeyCredentialRequestOptions::class) {
Expand All @@ -98,6 +100,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a
$format,
$context
),
$data['hints'] ?? [],
);
}
throw new BadMethodCallException('Unsupported type');
Expand Down Expand Up @@ -148,6 +151,7 @@ public function normalize(mixed $object, ?string $format = null, array $context
$format,
$context
),
'hints' => count($object->hints) === 0 ? null : $object->hints,
];

if ($object instanceof PublicKeyCredentialCreationOptions) {
Expand Down
9 changes: 7 additions & 2 deletions src/webauthn/src/PublicKeyCredentialCreationOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ final class PublicKeyCredentialCreationOptions extends PublicKeyCredentialOption
* @param PublicKeyCredentialParameters[] $pubKeyCredParams
* @param PublicKeyCredentialDescriptor[] $excludeCredentials
* @param null|positive-int $timeout
* @param string[] $hints
*/
public function __construct(
public readonly PublicKeyCredentialRpEntity $rp,
Expand All @@ -44,6 +45,7 @@ public function __construct(
public array $excludeCredentials = [],
null|int $timeout = null,
null|AuthenticationExtensions $extensions = null,
array $hints = [],
) {
foreach ($pubKeyCredParams as $pubKeyCredParam) {
$pubKeyCredParam instanceof PublicKeyCredentialParameters || throw new InvalidArgumentException(
Expand All @@ -60,13 +62,14 @@ public function __construct(
'Invalid attestation conveyance mode'
);

parent::__construct($challenge, $timeout, $extensions);
parent::__construct($challenge, $timeout, $extensions, $hints);
}

/**
* @param PublicKeyCredentialParameters[] $pubKeyCredParams
* @param PublicKeyCredentialDescriptor[] $excludeCredentials
* @param null|positive-int $timeout
* @param string[] $hints
*/
public static function create(
PublicKeyCredentialRpEntity $rp,
Expand All @@ -78,6 +81,7 @@ public static function create(
array $excludeCredentials = [],
null|int $timeout = null,
null|AuthenticationExtensions $extensions = null,
array $hints = [],
): self {
return new self(
$rp,
Expand All @@ -88,7 +92,8 @@ public static function create(
$attestation,
$excludeCredentials,
$timeout,
$extensions
$extensions,
$hints
);
}
}
20 changes: 20 additions & 0 deletions src/webauthn/src/PublicKeyCredentialOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,33 @@

use InvalidArgumentException;
use Webauthn\AuthenticationExtensions\AuthenticationExtensions;
use function in_array;
use function is_string;
use function sprintf;

abstract class PublicKeyCredentialOptions
{
public const HINT_SECURITY_KEY = 'security-key';

public const HINT_CLIENT_DEVICE = 'client-device';

public const HINT_HYBRID = 'hybrid';

public const HINTS = [self::HINT_SECURITY_KEY, self::HINT_CLIENT_DEVICE, self::HINT_HYBRID];

public AuthenticationExtensions $extensions;

/**
* @param positive-int|null $timeout
* @param null|AuthenticationExtensions|array<array-key, AuthenticationExtensions> $extensions
* @param string[] $hints
* @protected
*/
public function __construct(
public string $challenge,
public null|int $timeout = null,
null|array|AuthenticationExtensions $extensions = null,
public array $hints = [],
) {
($this->timeout === null || $this->timeout > 0) || throw new InvalidArgumentException('Invalid timeout');
if ($extensions === null) {
Expand All @@ -29,5 +42,12 @@ public function __construct(
} else {
$this->extensions = AuthenticationExtensions::create($extensions);
}

foreach ($this->hints as $hint) {
is_string($hint) || throw new InvalidArgumentException('Invalid hint: hints must be strings');
in_array($hint, self::HINTS, true) || throw new InvalidArgumentException(
sprintf('Invalid hint "%s". Allowed values are: %s', $hint, implode(', ', self::HINTS))
);
}
}
}
9 changes: 7 additions & 2 deletions src/webauthn/src/PublicKeyCredentialRequestOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ final class PublicKeyCredentialRequestOptions extends PublicKeyCredentialOptions
/**
* @param PublicKeyCredentialDescriptor[] $allowCredentials
* @param null|AuthenticationExtensions|array<array-key, mixed|AuthenticationExtensions> $extensions
* @param string[] $hints
*/
public function __construct(
string $challenge,
Expand All @@ -36,6 +37,7 @@ public function __construct(
public null|string $userVerification = null,
null|int $timeout = null,
null|array|AuthenticationExtensions $extensions = null,
array $hints = [],
) {
in_array($userVerification, self::USER_VERIFICATION_REQUIREMENTS, true) || throw InvalidDataException::create(
$userVerification,
Expand All @@ -44,14 +46,16 @@ public function __construct(
parent::__construct(
$challenge,
$timeout,
$extensions
$extensions,
$hints
);
}

/**
* @param PublicKeyCredentialDescriptor[] $allowCredentials
* @param positive-int $timeout
* @param null|AuthenticationExtensions|array<array-key, AuthenticationExtensions> $extensions
* @param string[] $hints
*/
public static function create(
string $challenge,
Expand All @@ -60,7 +64,8 @@ public static function create(
null|string $userVerification = null,
null|int $timeout = null,
null|array|AuthenticationExtensions $extensions = null,
array $hints = [],
): self {
return new self($challenge, $rpId, $allowCredentials, $userVerification, $timeout, $extensions);
return new self($challenge, $rpId, $allowCredentials, $userVerification, $timeout, $extensions, $hints);
}
}
79 changes: 79 additions & 0 deletions tests/library/Unit/PublicKeyCredentialCreationOptionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Webauthn\Tests\Unit;

use InvalidArgumentException;
use PHPUnit\Framework\Attributes\Test;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
use Webauthn\PublicKeyCredentialCreationOptions;
Expand Down Expand Up @@ -100,4 +101,82 @@ public function anPublicKeyCredentialCreationOptionsWithoutExcludeCredentialsCan
]);
static::assertSame([], $data->excludeCredentials);
}

#[Test]
public function anPublicKeyCredentialCreationOptionsWithHintsCanBeCreatedAndSerialized(): void
{
$rp = PublicKeyCredentialRpEntity::create();
$user = PublicKeyCredentialUserEntity::create('USER', 'id', 'FOO BAR');
$credentialParameters = PublicKeyCredentialParameters::create('type', -100);

$options = PublicKeyCredentialCreationOptions::create(
$rp,
$user,
'challenge',
[$credentialParameters],
attestation: PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
timeout: 1000,
hints: [
PublicKeyCredentialCreationOptions::HINT_SECURITY_KEY,
PublicKeyCredentialCreationOptions::HINT_HYBRID,
]
);

static::assertSame([
PublicKeyCredentialCreationOptions::HINT_SECURITY_KEY,
PublicKeyCredentialCreationOptions::HINT_HYBRID,
], $options->hints);

$json = $this->getSerializer()
->serialize($options, 'json', [
AbstractObjectNormalizer::SKIP_NULL_VALUES => true,
]);
static::assertJsonStringEqualsJsonString(
'{"user":{"name":"USER","id":"aWQ","displayName":"FOO BAR"},"excludeCredentials":[],"challenge":"Y2hhbGxlbmdl","pubKeyCredParams":[{"type":"type","alg":-100}],"timeout":1000,"attestation":"none","hints":["security-key","hybrid"]}',
$json
);

$data = $this->getSerializer()
->deserialize($json, PublicKeyCredentialCreationOptions::class, 'json');
static::assertSame(['security-key', 'hybrid'], $data->hints);
}

#[Test]
public function anPublicKeyCredentialCreationOptionsWithInvalidHintThrowsException(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(
'Invalid hint "invalid-hint". Allowed values are: security-key, client-device, hybrid'
);

$rp = PublicKeyCredentialRpEntity::create();
$user = PublicKeyCredentialUserEntity::create('USER', 'id', 'FOO BAR');
$credentialParameters = PublicKeyCredentialParameters::create('type', -100);

PublicKeyCredentialCreationOptions::create(
$rp,
$user,
'challenge',
[$credentialParameters],
hints: ['invalid-hint']
);
}

#[Test]
public function anPublicKeyCredentialCreationOptionsWithEmptyHintsCanBeCreated(): void
{
$rp = PublicKeyCredentialRpEntity::create();
$user = PublicKeyCredentialUserEntity::create('USER', 'id', 'FOO BAR');
$credentialParameters = PublicKeyCredentialParameters::create('type', -100);

$options = PublicKeyCredentialCreationOptions::create(
$rp,
$user,
'challenge',
[$credentialParameters],
hints: []
);

static::assertSame([], $options->hints);
}
}
Loading