Skip to content

feat: add opt-in support for millisecond timestamps - #644

Open
saifulferoz wants to merge 2 commits into
googleapis:mainfrom
saifulferoz:feat/millisecond-timestamps
Open

feat: add opt-in support for millisecond timestamps#644
saifulferoz wants to merge 2 commits into
googleapis:mainfrom
saifulferoz:feat/millisecond-timestamps

Conversation

@saifulferoz

Copy link
Copy Markdown

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.

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.
@cy-yun cy-yun self-assigned this Jul 30, 2026

@cy-yun cy-yun left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
}

Comment thread src/JWT.php
Comment thread tests/JWTTest.php Outdated
public function testValidTokenWithMillisecondTimestamps()
{
JWT::$useMillisecondTimestamps = true;
$nowMs = (int) (microtime(true) * 1000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
$nowMs = (int) (microtime(true) * 1000);
$nowMs = \round(\microtime(true) * 1000);

Comment thread tests/JWTTest.php Outdated
Comment thread tests/JWTTest.php Outdated
Comment thread tests/JWTTest.php Outdated
Comment thread src/JWT.php Outdated
saifulferoz added a commit to saifulferoz/php-jwt that referenced this pull request Jul 31, 2026
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().
@saifulferoz

Copy link
Copy Markdown
Author

Thanks for the careful review, @cy-yun — good catch on the 32-bit overflow. Pushed a fix:

  1. src/JWT.php: reference time now uses round(microtime(true) * 1000) so the millisecond value stays a float instead of overflowing PHP_INT_MAX on 32-bit PHP.
  2. src/ExpiredException.php: widened $timestamp and its setTimestamp()/getTimestamp() accessors to int|float so a float timestamp doesn't trigger a TypeError on PHP 8.0+.
  3. JWT::$timestamp docblock widened to int|float|null.
  4. tests/JWTTest.php: the millisecond timestamps are now built with round().

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().
@saifulferoz
saifulferoz force-pushed the feat/millisecond-timestamps branch from d25ed03 to 0f09955 Compare July 31, 2026 04:34

@saifulferoz saifulferoz left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All review comments have been addressed. Could you please re-review the PR?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants