feat: add opt-in support for millisecond timestamps - #644
Conversation
Some non-compliant token issuers emit the iat/nbf/exp claims in milliseconds rather than the seconds mandated by RFC 7519, which causes decode() to reject the token with a date far in the future (see googleapis#539). Add a JWT::$useMillisecondTimestamps flag (default false). When enabled, the reference time defaults to milliseconds (microtime(true) * 1000) so claims are compared in the same unit, and the BeforeValidException date messages are rendered from the correct second value.
There was a problem hiding this comment.
Thanks for putting this together! The implementation looks solid, but there is a critical edge case regarding 32-bit systems and the ExpiredException class that should be addressed in this PR.
Because millisecond timestamps (e.g., 1710000000000) exceed the 32-bit integer limit (PHP_INT_MAX is 2147483647), any code casting these timestamps to (int) will trigger an integer overflow and cause validation to fail completely on 32-bit PHP systems.
This creates a cascading issue with ExpiredException.php, which currently enforces a strict int type hint.
To ensure this new feature works reliably across all environments without causing static analysis errors or TypeErrors, I highly recommend implementing the in-line suggestions and the following:
Update ExpiredException.php to accept floats:
If a user (or the library itself) passes a float to avoid 32-bit integer overflow, ExpiredException::setTimestamp(int $timestamp) will trigger a fatal TypeError in PHP 8.1+ on 32-bit systems. Since the library requires PHP ^8.0, you can safely use union types here:
// src/ExpiredException.php
public function setTimestamp(int|float $timestamp): void
{
$this->timestamp = $timestamp;
}
public function getTimestamp(): int|float|null
{
return $this->timestamp;
}| public function testValidTokenWithMillisecondTimestamps() | ||
| { | ||
| JWT::$useMillisecondTimestamps = true; | ||
| $nowMs = (int) (microtime(true) * 1000); |
There was a problem hiding this comment.
Similar to JWT.php, casting to (int) will overflow on 32-bit systems and fail the tests.
Remove the (int) cast and use \round() to ensure a whole number.
| $nowMs = (int) (microtime(true) * 1000); | |
| $nowMs = \round(\microtime(true) * 1000); |
Address review feedback on googleapis#644: millisecond timestamps exceed PHP_INT_MAX on 32-bit systems, so casting to (int) overflows and breaks validation. - Use round() instead of an (int) cast so the reference time stays a float in millisecond mode. - Widen ExpiredException::$timestamp and its accessors to int|float. - Widen JWT::$timestamp docblock to int|float|null. - Update tests to build millisecond timestamps with round().
|
Thanks for the careful review, @cy-yun — good catch on the 32-bit overflow. Pushed a fix:
Full suite is green (123 tests, 293 assertions). Note the overflow path only manifests on 32-bit PHP, so it's fixed by reasoning rather than exercised by CI here — happy to add a dedicated case if you'd prefer. Let me know if anything else needs adjusting. |
Address review feedback on googleapis#644: millisecond timestamps exceed PHP_INT_MAX on 32-bit systems, so casting to (int) overflows and breaks validation. - Use round() instead of an (int) cast so the reference time stays a float in millisecond mode. - Widen ExpiredException::$timestamp and its accessors to the int|float union type. - Widen JWT::$timestamp docblock to int|float|null. - Update tests to build millisecond timestamps with round().
d25ed03 to
0f09955
Compare
saifulferoz
left a comment
There was a problem hiding this comment.
All review comments have been addressed. Could you please re-review the PR?
Some non-compliant token issuers emit the iat/nbf/exp claims in milliseconds rather than the seconds mandated by RFC 7519, which causes decode() to reject the token with a date far in the future (see #539).
Add a JWT::$useMillisecondTimestamps flag (default false). When enabled, the reference time defaults to milliseconds (microtime(true) * 1000) so claims are compared in the same unit, and the BeforeValidException date messages are rendered from the correct second value.