diff --git a/src/Http/AuthStrategy/SignedRequestStrategy.php b/src/Http/AuthStrategy/SignedRequestStrategy.php index d27ae3c6..97327cb2 100644 --- a/src/Http/AuthStrategy/SignedRequestStrategy.php +++ b/src/Http/AuthStrategy/SignedRequestStrategy.php @@ -61,7 +61,7 @@ public function createQueryParams(): array { $params = [ 'nonce' => self::generateNonce(), - 'timestamp' => (string)(round(microtime(true) * 1000)), + 'timestamp' => sprintf('%.0F', microtime(true) * 1000), ]; if ($this->sdkId !== null) { diff --git a/tests/Http/AuthStrategy/SignedRequestStrategyTest.php b/tests/Http/AuthStrategy/SignedRequestStrategyTest.php index 1d44b54b..61418386 100644 --- a/tests/Http/AuthStrategy/SignedRequestStrategyTest.php +++ b/tests/Http/AuthStrategy/SignedRequestStrategyTest.php @@ -68,6 +68,72 @@ public function shouldReturnNonceAndTimestampQueryParams() $this->assertNotEmpty($params['timestamp']); } + /** + * @test + * @covers ::createQueryParams + * @dataProvider lowPrecisionProvider + * + * On PHP 8.4 FPM environments (e.g. WP Engine) the `precision` ini setting + * can cause (string)(round(microtime(true) * 1000)) to emit scientific notation + * (e.g. "1.78231576830E+12"), which breaks Yoti signature verification with 401. + */ + public function shouldReturnTimestampAsPlainIntegerStringUnderLowPrecision(string $precision) + { + $strategy = new SignedRequestStrategy(PemFile::fromFilePath(TestData::PEM_FILE)); + + $originalPrecision = ini_get('precision'); + if ($originalPrecision === false || ini_set('precision', $precision) === false) { + $this->markTestSkipped('Unable to set ini precision for this runtime'); + } + + try { + $params = $strategy->createQueryParams(); + } finally { + ini_set('precision', (string) $originalPrecision); + } + + $this->assertMatchesRegularExpression( + '/^\d+$/', + $params['timestamp'], + "Timestamp must be a plain integer string (no scientific notation) at precision={$precision}" + ); + } + + public function lowPrecisionProvider(): array + { + return [ + 'precision=12' => ['12'], + 'precision=8' => ['8'], + ]; + } + + /** + * @test + * @covers ::createQueryParams + */ + public function shouldReturnTimestampAsUnixMilliseconds() + { + $strategy = new SignedRequestStrategy(PemFile::fromFilePath(TestData::PEM_FILE)); + + $originalPrecision = ini_get('precision'); + if ($originalPrecision === false || ini_set('precision', '8') === false) { + $this->markTestSkipped('Unable to set ini precision for this runtime'); + } + + try { + $beforeMs = (int) floor(microtime(true) * 1000); + $params = $strategy->createQueryParams(); + $afterMs = (int) ceil(microtime(true) * 1000); + } finally { + ini_set('precision', (string) $originalPrecision); + } + + // Assert plain integer format first — (int) cast alone would mask scientific notation + $this->assertMatchesRegularExpression('/^\d+$/', $params['timestamp']); + $this->assertGreaterThanOrEqual($beforeMs, (int) $params['timestamp']); + $this->assertLessThanOrEqual($afterMs, (int) $params['timestamp']); + } + /** * @test * @covers ::createQueryParams diff --git a/tests/Http/ClientTest.php b/tests/Http/ClientTest.php index 7fd38e82..58f77977 100644 --- a/tests/Http/ClientTest.php +++ b/tests/Http/ClientTest.php @@ -37,7 +37,7 @@ public function testSendRequest() $this->assertSame( $someResponse, - $client->sendRequest(new Request('GET', '/')) + $client->sendRequest(new Request('GET', 'https://api.yoti.com/')) ); $this->assertEquals(30, $someHandler->getLastOptions()['timeout']); @@ -138,6 +138,6 @@ private function sendRequestAndThrow(\Throwable $exception) $client = new Client(['handler' => $someHandlerStack]); - $client->sendRequest(new Request('GET', '/')); + $client->sendRequest(new Request('GET', 'https://api.yoti.com/')); } }