diff --git a/src/Challenge.php b/src/Challenge.php index 4d8797c..d80dc70 100644 --- a/src/Challenge.php +++ b/src/Challenge.php @@ -4,6 +4,8 @@ namespace Firehed\WebAuthn; +use DateTimeImmutable; + /** * The Challenge object has limited public-facing API: * - Create a challenge through the `::random()` method @@ -55,6 +57,11 @@ public function getBase64Url(): string return $this->wrapped->toBase64Url(); } + public function getExpiration(): ?DateTimeImmutable + { + return null; + } + /** * @return SerializationFormat */ diff --git a/src/ChallengeInterface.php b/src/ChallengeInterface.php index 6375125..fce2b30 100644 --- a/src/ChallengeInterface.php +++ b/src/ChallengeInterface.php @@ -4,6 +4,8 @@ namespace Firehed\WebAuthn; +use DateTimeImmutable; + interface ChallengeInterface { /** @@ -44,4 +46,17 @@ public function getBase64Url(): string; * @internal */ public function getBinary(): BinaryString; + + /** + * If non-null, indicates when the challenge should be considered expired. + * This should be used in conjunction with request generation and align + * with the `timeout` used by `pkOptions`. Be aware that browsers may + * override the specified value; the current W3C recommendation (lv3) is + * between 5 and 10 minutes (300-600 seconds). + * + * At present, this is intended as a convenience for storage mechanisms and + * expected to be enforced by _ChallengeInterface_ implemetions, not the RP + * server and associated internals. + */ + public function getExpiration(): ?DateTimeImmutable; } diff --git a/src/ExpiringChallenge.php b/src/ExpiringChallenge.php index d45c3b0..e38551a 100644 --- a/src/ExpiringChallenge.php +++ b/src/ExpiringChallenge.php @@ -4,7 +4,6 @@ namespace Firehed\WebAuthn; -use DateTimeInterface; use DateInterval; use DateTimeImmutable; use InvalidArgumentException; @@ -18,13 +17,13 @@ * * @phpstan-type SerializationFormat array{ * c: string, - * e: int, + * e: numeric-string, * } */ class ExpiringChallenge implements ChallengeInterface { private ChallengeInterface $wrapped; - private DateTimeInterface $expiration; + private DateTimeImmutable $expiration; /** * @internal @@ -74,6 +73,11 @@ public function getBinary(): BinaryString return $this->wrapped->getBinary(); } + public function getExpiration(): DateTimeImmutable + { + return $this->expiration; + } + private function isExpired(): bool { $diff = $this->expiration->diff(new DateTimeImmutable()); @@ -88,7 +92,7 @@ public function __serialize(): array { return [ 'c' => $this->wrapped->getBase64(), - 'e' => $this->expiration->getTimestamp(), + 'e' => $this->expiration->format('U.u'), ]; } diff --git a/tests/ChallengeInterfaceTestTrait.php b/tests/ChallengeInterfaceTestTrait.php index 86eb7b3..9e8336d 100644 --- a/tests/ChallengeInterfaceTestTrait.php +++ b/tests/ChallengeInterfaceTestTrait.php @@ -30,6 +30,11 @@ public function testSerializationRoundTrip(): void $unserialized->getBase64(), 'Base64 changed', ); + + self::assertEquals( + $challenge->getExpiration(), + $unserialized->getExpiration(), + ); } public function testBinaryMatchesBase64(): void diff --git a/tests/TestUtilities/TestVectorFixedChallenge.php b/tests/TestUtilities/TestVectorFixedChallenge.php index b7a4289..a721515 100644 --- a/tests/TestUtilities/TestVectorFixedChallenge.php +++ b/tests/TestUtilities/TestVectorFixedChallenge.php @@ -4,6 +4,7 @@ namespace Firehed\WebAuthn\TestUtilities; +use DateTimeImmutable; use Exception; use Firehed\WebAuthn\{ BinaryString, @@ -21,6 +22,11 @@ public function __construct(private string $b64u) { } + public function getExpiration(): ?DateTimeImmutable + { + return null; + } + public function getBinary(): BinaryString { return BinaryString::fromBase64Url($this->b64u);