From 19b9123fea4b379446e94cceec5df24cbee53b0b Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Mon, 5 Aug 2024 18:04:54 -0700 Subject: [PATCH] Add latest fields to Credential object (#17) This updates Credential to the latest API spec, including defining a new `enum` to cover the transports. --- composer.json | 2 +- phpunit.xml | 12 ++--- src/Credential.php | 48 ++++++++++++++++- src/WebAuthn/AuthenticatorTransport.php | 42 +++++++++++++++ src/WebAuthn/README.md | 5 ++ tests/CredentialTest.php | 72 +++++++++++++++++++++++++ tests/fixtures/credential1.json | 15 ++++++ tests/fixtures/credential2.json | 13 +++++ 8 files changed, 200 insertions(+), 9 deletions(-) create mode 100644 src/WebAuthn/AuthenticatorTransport.php create mode 100644 src/WebAuthn/README.md create mode 100644 tests/CredentialTest.php create mode 100644 tests/fixtures/credential1.json create mode 100644 tests/fixtures/credential2.json diff --git a/composer.json b/composer.json index f033ec4..aa74856 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ "phpstan/phpstan": "^1.0", "phpstan/phpstan-phpunit": "^1.0", "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.3 || ^10.0", + "phpunit/phpunit": "^10.0", "squizlabs/php_codesniffer": "^3.5" }, "conflict": { diff --git a/phpunit.xml b/phpunit.xml index 7be976b..41d88c5 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,22 +1,20 @@ + cacheDirectory=".phpunit.cache" + requireCoverageMetadata="true" + beStrictAboutCoverageMetadata="true"> tests - - + src diff --git a/src/Credential.php b/src/Credential.php index 27c1994..5a56957 100644 --- a/src/Credential.php +++ b/src/Credential.php @@ -4,13 +4,59 @@ namespace SnapAuth; +use DateTimeImmutable; + class Credential { public readonly string $id; + public readonly string $aaguid; + public readonly string $name; + public readonly bool $isActive; + public readonly bool $isBackedUp; + public readonly bool $isBackupEligible; + public readonly bool $isUvInitialized; + public readonly DateTimeImmutable $createdAt; + /** + * @var WebAuthn\AuthenticatorTransport[] + */ + public readonly array $transports; - // @phpstan-ignore-next-line + /** + * @internal The Credential object is part of the SDK, but its constructor + * is not part of SemVer scope. + * + * @phpstan-ignore-next-line + * + * This is the correct shape: + * param array{ + * id: string, + * aaguid: string, + * name: string, + * isActive: bool, + * isBackedUp: bool, + * isBackupEligible: bool, + * isUvInitialized: bool, + * createdAt: int, + * transports: string[], + * } $data + */ public function __construct(array $data) { $this->id = $data['id']; + $this->aaguid = $data['aaguid']; + $this->name = $data['name']; + $this->isActive = $data['isActive']; + $this->isBackedUp = $data['isBackedUp']; + $this->isBackupEligible = $data['isBackupEligible']; + $this->isUvInitialized = $data['isUvInitialized']; + + $this->createdAt = (new DateTimeImmutable())->setTimestamp($data['createdAt']); + + // Ensure array_is_list if anything is filtered + $this->transports = array_values(array_filter( + // If other transport methods are added on the API (which itself + // requires a WebAuthn spec bump), filter out unknown values + array_map(WebAuthn\AuthenticatorTransport::tryFrom(...), $data['transports']) + )); } } diff --git a/src/WebAuthn/AuthenticatorTransport.php b/src/WebAuthn/AuthenticatorTransport.php new file mode 100644 index 0000000..54bf36b --- /dev/null +++ b/src/WebAuthn/AuthenticatorTransport.php @@ -0,0 +1,42 @@ +readFixture('credential1.json'); + $cred = new Credential($data); + + self::assertSame('ctl_2893f2Vg86463c8xV7wVv5PG', $cred->id); + self::assertSame('fbfc3007-154e-4ecc-8c0b-6e020557d7bd', $cred->aaguid); + self::assertTrue($cred->isActive); + self::assertTrue($cred->isBackedUp); + self::assertTrue($cred->isBackupEligible); + self::assertTrue($cred->isUvInitialized); + self::assertSame('iCloud Keychain', $cred->name); + self::assertSame([ + WebAuthn\AuthenticatorTransport::Hybrid, + WebAuthn\AuthenticatorTransport::Internal, + ], $cred->transports); + self::assertEquals(new DateTimeImmutable('2024-03-07T20:02:04Z'), $cred->createdAt); + } + + public function testDecodingUsbFromApiResponse(): void + { + $data = $this->readFixture('credential2.json'); + $cred = new Credential($data); + + self::assertSame('ctl_28CWCw4G3R4MGCg2cc2ccvGr', $cred->id); + self::assertSame('00000000-0000-0000-0000-000000000000', $cred->aaguid); + self::assertTrue($cred->isActive); + self::assertFalse($cred->isBackedUp); + self::assertFalse($cred->isBackupEligible); + self::assertFalse($cred->isUvInitialized); + self::assertSame('Passkey', $cred->name); + self::assertSame([WebAuthn\AuthenticatorTransport::Usb], $cred->transports); + self::assertEquals(new DateTimeImmutable('2024-08-05T21:35:48Z'), $cred->createdAt); + } + + /** + * @return mixed[] + */ + private function readFixture(string $path): array + { + $path = sprintf('%s/%s/%s', __DIR__, 'fixtures', $path); + $json = file_get_contents($path); + assert($json !== false); + $data = json_decode($json, true, flags: JSON_THROW_ON_ERROR); + assert(is_array($data)); + return $data; + } +} diff --git a/tests/fixtures/credential1.json b/tests/fixtures/credential1.json new file mode 100644 index 0000000..9b39d14 --- /dev/null +++ b/tests/fixtures/credential1.json @@ -0,0 +1,15 @@ +{ + "id": "ctl_2893f2Vg86463c8xV7wVv5PG", + "aaguid": "fbfc3007-154e-4ecc-8c0b-6e020557d7bd", + "isActive": true, + "isBackedUp": true, + "isBackupEligible": true, + "isUvInitialized": true, + "name": "iCloud Keychain", + "transports": [ + "hybrid", + "unknown_ignoreme", + "internal" + ], + "createdAt": 1709841724 +} diff --git a/tests/fixtures/credential2.json b/tests/fixtures/credential2.json new file mode 100644 index 0000000..7143e7c --- /dev/null +++ b/tests/fixtures/credential2.json @@ -0,0 +1,13 @@ +{ + "id": "ctl_28CWCw4G3R4MGCg2cc2ccvGr", + "aaguid": "00000000-0000-0000-0000-000000000000", + "isActive": true, + "isBackedUp": false, + "isBackupEligible": false, + "isUvInitialized": false, + "name": "Passkey", + "transports": [ + "usb" + ], + "createdAt": 1722893748 +}