Skip to content

Commit 19b9123

Browse files
authored
Add latest fields to Credential object (#17)
This updates Credential to the latest API spec, including defining a new `enum` to cover the transports.
1 parent 820ae71 commit 19b9123

File tree

8 files changed

+200
-9
lines changed

8 files changed

+200
-9
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"phpstan/phpstan": "^1.0",
3535
"phpstan/phpstan-phpunit": "^1.0",
3636
"phpstan/phpstan-strict-rules": "^1.0",
37-
"phpunit/phpunit": "^9.3 || ^10.0",
37+
"phpunit/phpunit": "^10.0",
3838
"squizlabs/php_codesniffer": "^3.5"
3939
},
4040
"conflict": {

phpunit.xml

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3-
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
3+
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd"
44
bootstrap="vendor/autoload.php"
55
executionOrder="depends,defects"
6-
forceCoversAnnotation="true"
7-
beStrictAboutCoversAnnotation="true"
86
beStrictAboutOutputDuringTests="true"
9-
beStrictAboutTodoAnnotatedTests="true"
107
failOnRisky="true"
118
failOnWarning="true"
12-
verbose="true">
9+
cacheDirectory=".phpunit.cache"
10+
requireCoverageMetadata="true"
11+
beStrictAboutCoverageMetadata="true">
1312
<testsuites>
1413
<testsuite name="default">
1514
<directory suffix="Test.php">tests</directory>
1615
</testsuite>
1716
</testsuites>
18-
19-
<coverage processUncoveredFiles="true">
17+
<coverage>
2018
<include>
2119
<directory suffix=".php">src</directory>
2220
</include>

src/Credential.php

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,59 @@
44

55
namespace SnapAuth;
66

7+
use DateTimeImmutable;
8+
79
class Credential
810
{
911
public readonly string $id;
12+
public readonly string $aaguid;
13+
public readonly string $name;
14+
public readonly bool $isActive;
15+
public readonly bool $isBackedUp;
16+
public readonly bool $isBackupEligible;
17+
public readonly bool $isUvInitialized;
18+
public readonly DateTimeImmutable $createdAt;
19+
/**
20+
* @var WebAuthn\AuthenticatorTransport[]
21+
*/
22+
public readonly array $transports;
1023

11-
// @phpstan-ignore-next-line
24+
/**
25+
* @internal The Credential object is part of the SDK, but its constructor
26+
* is not part of SemVer scope.
27+
*
28+
* @phpstan-ignore-next-line
29+
*
30+
* This is the correct shape:
31+
* param array{
32+
* id: string,
33+
* aaguid: string,
34+
* name: string,
35+
* isActive: bool,
36+
* isBackedUp: bool,
37+
* isBackupEligible: bool,
38+
* isUvInitialized: bool,
39+
* createdAt: int,
40+
* transports: string[],
41+
* } $data
42+
*/
1243
public function __construct(array $data)
1344
{
1445
$this->id = $data['id'];
46+
$this->aaguid = $data['aaguid'];
47+
$this->name = $data['name'];
48+
$this->isActive = $data['isActive'];
49+
$this->isBackedUp = $data['isBackedUp'];
50+
$this->isBackupEligible = $data['isBackupEligible'];
51+
$this->isUvInitialized = $data['isUvInitialized'];
52+
53+
$this->createdAt = (new DateTimeImmutable())->setTimestamp($data['createdAt']);
54+
55+
// Ensure array_is_list if anything is filtered
56+
$this->transports = array_values(array_filter(
57+
// If other transport methods are added on the API (which itself
58+
// requires a WebAuthn spec bump), filter out unknown values
59+
array_map(WebAuthn\AuthenticatorTransport::tryFrom(...), $data['transports'])
60+
));
1561
}
1662
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SnapAuth\WebAuthn;
6+
7+
/**
8+
* @link https://www.w3.org/TR/webauthn-3/#enum-transport
9+
*/
10+
enum AuthenticatorTransport: string
11+
{
12+
/**
13+
* Bluetooth Low Energy
14+
*/
15+
case Ble = 'ble';
16+
17+
/**
18+
* Smart Cards
19+
*/
20+
case SmartCard = 'smart-card';
21+
22+
/**
23+
* Mixed transport methods, including (but not limited to) Cross-Device
24+
* Authentication
25+
*/
26+
case Hybrid = 'hybrid';
27+
28+
/**
29+
* Platform authenticators, such as system-managed credential managers
30+
*/
31+
case Internal = 'internal';
32+
33+
/**
34+
* Near-Field Communication
35+
*/
36+
case Nfc = 'nfc';
37+
38+
/**
39+
* Removable USB devices
40+
*/
41+
case Usb = 'usb';
42+
}

src/WebAuthn/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Web Authentication enums
2+
3+
The enumerations in this directory are direct representations of those from the WebAuthn spec.
4+
5+
See https://www.w3.org/TR/webauthn-3/

tests/CredentialTest.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SnapAuth;
6+
7+
use DateTimeImmutable;
8+
use PHPUnit\Framework\Attributes\CoversClass;
9+
use PHPUnit\Framework\Attributes\Small;
10+
use PHPUnit\Framework\MockObject\MockObject;
11+
use PHPUnit\Framework\TestCase;
12+
13+
use function assert;
14+
use function file_get_contents;
15+
use function is_array;
16+
use function json_decode;
17+
use function sprintf;
18+
19+
use const JSON_THROW_ON_ERROR;
20+
21+
#[CoversClass(Credential::class)]
22+
#[Small]
23+
class CredentialTest extends TestCase
24+
{
25+
public function testDecodingFromApiResponse(): void
26+
{
27+
$data = $this->readFixture('credential1.json');
28+
$cred = new Credential($data);
29+
30+
self::assertSame('ctl_2893f2Vg86463c8xV7wVv5PG', $cred->id);
31+
self::assertSame('fbfc3007-154e-4ecc-8c0b-6e020557d7bd', $cred->aaguid);
32+
self::assertTrue($cred->isActive);
33+
self::assertTrue($cred->isBackedUp);
34+
self::assertTrue($cred->isBackupEligible);
35+
self::assertTrue($cred->isUvInitialized);
36+
self::assertSame('iCloud Keychain', $cred->name);
37+
self::assertSame([
38+
WebAuthn\AuthenticatorTransport::Hybrid,
39+
WebAuthn\AuthenticatorTransport::Internal,
40+
], $cred->transports);
41+
self::assertEquals(new DateTimeImmutable('2024-03-07T20:02:04Z'), $cred->createdAt);
42+
}
43+
44+
public function testDecodingUsbFromApiResponse(): void
45+
{
46+
$data = $this->readFixture('credential2.json');
47+
$cred = new Credential($data);
48+
49+
self::assertSame('ctl_28CWCw4G3R4MGCg2cc2ccvGr', $cred->id);
50+
self::assertSame('00000000-0000-0000-0000-000000000000', $cred->aaguid);
51+
self::assertTrue($cred->isActive);
52+
self::assertFalse($cred->isBackedUp);
53+
self::assertFalse($cred->isBackupEligible);
54+
self::assertFalse($cred->isUvInitialized);
55+
self::assertSame('Passkey', $cred->name);
56+
self::assertSame([WebAuthn\AuthenticatorTransport::Usb], $cred->transports);
57+
self::assertEquals(new DateTimeImmutable('2024-08-05T21:35:48Z'), $cred->createdAt);
58+
}
59+
60+
/**
61+
* @return mixed[]
62+
*/
63+
private function readFixture(string $path): array
64+
{
65+
$path = sprintf('%s/%s/%s', __DIR__, 'fixtures', $path);
66+
$json = file_get_contents($path);
67+
assert($json !== false);
68+
$data = json_decode($json, true, flags: JSON_THROW_ON_ERROR);
69+
assert(is_array($data));
70+
return $data;
71+
}
72+
}

tests/fixtures/credential1.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"id": "ctl_2893f2Vg86463c8xV7wVv5PG",
3+
"aaguid": "fbfc3007-154e-4ecc-8c0b-6e020557d7bd",
4+
"isActive": true,
5+
"isBackedUp": true,
6+
"isBackupEligible": true,
7+
"isUvInitialized": true,
8+
"name": "iCloud Keychain",
9+
"transports": [
10+
"hybrid",
11+
"unknown_ignoreme",
12+
"internal"
13+
],
14+
"createdAt": 1709841724
15+
}

tests/fixtures/credential2.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"id": "ctl_28CWCw4G3R4MGCg2cc2ccvGr",
3+
"aaguid": "00000000-0000-0000-0000-000000000000",
4+
"isActive": true,
5+
"isBackedUp": false,
6+
"isBackupEligible": false,
7+
"isUvInitialized": false,
8+
"name": "Passkey",
9+
"transports": [
10+
"usb"
11+
],
12+
"createdAt": 1722893748
13+
}

0 commit comments

Comments
 (0)