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..33f432ae 100644 --- a/tests/Http/AuthStrategy/SignedRequestStrategyTest.php +++ b/tests/Http/AuthStrategy/SignedRequestStrategyTest.php @@ -68,6 +68,68 @@ 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): void + { + $strategy = new SignedRequestStrategy(PemFile::fromFilePath(TestData::PEM_FILE)); + + $originalPrecision = ini_get('precision'); + ini_set('precision', $precision); + + try { + $params = $strategy->createQueryParams(); + } finally { + ini_set('precision', $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(): void + { + $strategy = new SignedRequestStrategy(PemFile::fromFilePath(TestData::PEM_FILE)); + + $originalPrecision = ini_get('precision'); + ini_set('precision', '8'); + + try { + $beforeMs = (int) floor(microtime(true) * 1000); + $params = $strategy->createQueryParams(); + $afterMs = (int) ceil(microtime(true) * 1000); + } finally { + ini_set('precision', $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