Skip to content

Commit 3a3ca68

Browse files
committed
fix: prevent scientific notation in timestamp on low-precision PHP environments
On PHP FPM environments with non-default precision ini settings (≤12), casting a large float to string could produce scientific notation (e.g. 1.78E+12) instead of a plain integer, causing Yoti signature verification to fail with 401 MESSAGE_SIGNING. Replace (string)(round(microtime(true) * 1000)) with sprintf('%.0F', microtime(true) * 1000) which forces a plain decimal string regardless of the precision ini setting, and drops the redundant round() since %.0F already rounds to zero decimal places. Tests added to verify plain integer output under precision=12 and precision=8 (via @dataProvider), and to assert the timestamp falls within the correct Unix-millisecond range under low precision without masking scientific notation via an early (int) cast. Fixes #417
1 parent d4cc170 commit 3a3ca68

2 files changed

Lines changed: 63 additions & 1 deletion

File tree

src/Http/AuthStrategy/SignedRequestStrategy.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public function createQueryParams(): array
6161
{
6262
$params = [
6363
'nonce' => self::generateNonce(),
64-
'timestamp' => (string)(round(microtime(true) * 1000)),
64+
'timestamp' => sprintf('%.0F', microtime(true) * 1000),
6565
];
6666

6767
if ($this->sdkId !== null) {

tests/Http/AuthStrategy/SignedRequestStrategyTest.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,68 @@ public function shouldReturnNonceAndTimestampQueryParams()
6868
$this->assertNotEmpty($params['timestamp']);
6969
}
7070

71+
/**
72+
* @test
73+
* @covers ::createQueryParams
74+
* @dataProvider lowPrecisionProvider
75+
*
76+
* On PHP 8.4 FPM environments (e.g. WP Engine) the `precision` ini setting
77+
* can cause (string)(round(microtime(true) * 1000)) to emit scientific notation
78+
* (e.g. "1.78231576830E+12"), which breaks Yoti signature verification with 401.
79+
*/
80+
public function shouldReturnTimestampAsPlainIntegerStringUnderLowPrecision(string $precision): void
81+
{
82+
$strategy = new SignedRequestStrategy(PemFile::fromFilePath(TestData::PEM_FILE));
83+
84+
$originalPrecision = ini_get('precision');
85+
ini_set('precision', $precision);
86+
87+
try {
88+
$params = $strategy->createQueryParams();
89+
} finally {
90+
ini_set('precision', $originalPrecision);
91+
}
92+
93+
$this->assertMatchesRegularExpression(
94+
'/^\d+$/',
95+
$params['timestamp'],
96+
"Timestamp must be a plain integer string (no scientific notation) at precision={$precision}"
97+
);
98+
}
99+
100+
public function lowPrecisionProvider(): array
101+
{
102+
return [
103+
'precision=12' => ['12'],
104+
'precision=8' => ['8'],
105+
];
106+
}
107+
108+
/**
109+
* @test
110+
* @covers ::createQueryParams
111+
*/
112+
public function shouldReturnTimestampAsUnixMilliseconds(): void
113+
{
114+
$strategy = new SignedRequestStrategy(PemFile::fromFilePath(TestData::PEM_FILE));
115+
116+
$originalPrecision = ini_get('precision');
117+
ini_set('precision', '8');
118+
119+
try {
120+
$beforeMs = (int) floor(microtime(true) * 1000);
121+
$params = $strategy->createQueryParams();
122+
$afterMs = (int) ceil(microtime(true) * 1000);
123+
} finally {
124+
ini_set('precision', $originalPrecision);
125+
}
126+
127+
// Assert plain integer format first — (int) cast alone would mask scientific notation
128+
$this->assertMatchesRegularExpression('/^\d+$/', $params['timestamp']);
129+
$this->assertGreaterThanOrEqual($beforeMs, (int) $params['timestamp']);
130+
$this->assertLessThanOrEqual($afterMs, (int) $params['timestamp']);
131+
}
132+
71133
/**
72134
* @test
73135
* @covers ::createQueryParams

0 commit comments

Comments
 (0)