Skip to content

Commit

Permalink
Add latest fields to Credential object (#17)
Browse files Browse the repository at this point in the history
This updates Credential to the latest API spec, including defining a new
`enum` to cover the transports.
  • Loading branch information
Firehed authored Aug 6, 2024
1 parent 820ae71 commit 19b9123
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 9 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
12 changes: 5 additions & 7 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd"
bootstrap="vendor/autoload.php"
executionOrder="depends,defects"
forceCoversAnnotation="true"
beStrictAboutCoversAnnotation="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTodoAnnotatedTests="true"
failOnRisky="true"
failOnWarning="true"
verbose="true">
cacheDirectory=".phpunit.cache"
requireCoverageMetadata="true"
beStrictAboutCoverageMetadata="true">
<testsuites>
<testsuite name="default">
<directory suffix="Test.php">tests</directory>
</testsuite>
</testsuites>

<coverage processUncoveredFiles="true">
<coverage>
<include>
<directory suffix=".php">src</directory>
</include>
Expand Down
48 changes: 47 additions & 1 deletion src/Credential.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
));
}
}
42 changes: 42 additions & 0 deletions src/WebAuthn/AuthenticatorTransport.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace SnapAuth\WebAuthn;

/**
* @link https://www.w3.org/TR/webauthn-3/#enum-transport
*/
enum AuthenticatorTransport: string
{
/**
* Bluetooth Low Energy
*/
case Ble = 'ble';

/**
* Smart Cards
*/
case SmartCard = 'smart-card';

/**
* Mixed transport methods, including (but not limited to) Cross-Device
* Authentication
*/
case Hybrid = 'hybrid';

/**
* Platform authenticators, such as system-managed credential managers
*/
case Internal = 'internal';

/**
* Near-Field Communication
*/
case Nfc = 'nfc';

/**
* Removable USB devices
*/
case Usb = 'usb';
}
5 changes: 5 additions & 0 deletions src/WebAuthn/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Web Authentication enums

The enumerations in this directory are direct representations of those from the WebAuthn spec.

See https://www.w3.org/TR/webauthn-3/
72 changes: 72 additions & 0 deletions tests/CredentialTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

namespace SnapAuth;

use DateTimeImmutable;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Small;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

use function assert;
use function file_get_contents;
use function is_array;
use function json_decode;
use function sprintf;

use const JSON_THROW_ON_ERROR;

#[CoversClass(Credential::class)]
#[Small]
class CredentialTest extends TestCase
{
public function testDecodingFromApiResponse(): void
{
$data = $this->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;
}
}
15 changes: 15 additions & 0 deletions tests/fixtures/credential1.json
Original file line number Diff line number Diff line change
@@ -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
}
13 changes: 13 additions & 0 deletions tests/fixtures/credential2.json
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 19b9123

Please sign in to comment.