Skip to content

Commit ae4165a

Browse files
authored
Merge pull request #710 from lcobucci/guard-against-potential-precision-issues
Prevent type conversions issues when parsing time-fractions
2 parents cefaeb3 + c0938eb commit ae4165a

File tree

3 files changed

+136
-14
lines changed

3 files changed

+136
-14
lines changed

src/Token/Parser.php

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,13 @@
1212
use function count;
1313
use function explode;
1414
use function is_array;
15-
use function is_string;
16-
use function json_encode;
17-
use function strpos;
18-
19-
use const JSON_THROW_ON_ERROR;
15+
use function is_numeric;
16+
use function number_format;
2017

2118
final class Parser implements ParserInterface
2219
{
20+
private const MICROSECOND_PRECISION = 6;
21+
2322
private Decoder $decoder;
2423

2524
public function __construct(Decoder $decoder)
@@ -109,25 +108,29 @@ private function parseClaims(string $data): array
109108
continue;
110109
}
111110

112-
$date = $claims[$claim];
113-
114-
$claims[$claim] = $this->convertDate(is_string($date) ? $date : json_encode($date, JSON_THROW_ON_ERROR));
111+
$claims[$claim] = $this->convertDate($claims[$claim]);
115112
}
116113

117114
return $claims;
118115
}
119116

120-
/** @throws InvalidTokenStructure */
121-
private function convertDate(string $value): DateTimeImmutable
117+
/**
118+
* @param int|float|string $timestamp
119+
*
120+
* @throws InvalidTokenStructure
121+
*/
122+
private function convertDate($timestamp): DateTimeImmutable
122123
{
123-
if (strpos($value, '.') === false) {
124-
return new DateTimeImmutable('@' . $value);
124+
if (! is_numeric($timestamp)) {
125+
throw InvalidTokenStructure::dateIsNotParseable($timestamp);
125126
}
126127

127-
$date = DateTimeImmutable::createFromFormat('U.u', $value);
128+
$normalizedTimestamp = number_format((float) $timestamp, self::MICROSECOND_PRECISION, '.', '');
129+
130+
$date = DateTimeImmutable::createFromFormat('U.u', $normalizedTimestamp);
128131

129132
if ($date === false) {
130-
throw InvalidTokenStructure::dateIsNotParseable($value);
133+
throw InvalidTokenStructure::dateIsNotParseable($normalizedTimestamp);
131134
}
132135

133136
return $date;

test/functional/TimeFractionPrecisionTest.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
use DateTimeImmutable;
77
use Lcobucci\JWT\Configuration;
8+
use Lcobucci\JWT\Encoding\JoseEncoder;
89
use Lcobucci\JWT\Token\Plain;
910
use PHPUnit\Framework\TestCase;
1011

@@ -58,4 +59,42 @@ public function datesWithPotentialRoundingIssues(): iterable
5859
yield ['1613938511.018045'];
5960
yield ['1616074725.008455'];
6061
}
62+
63+
/**
64+
* @test
65+
* @dataProvider timeFractionConversions
66+
*
67+
* @param float|int|string $issuedAt
68+
*/
69+
public function typeConversionDoesNotCauseParsingErrors($issuedAt, string $timeFraction): void
70+
{
71+
$encoder = new JoseEncoder();
72+
$headers = $encoder->base64UrlEncode($encoder->jsonEncode(['typ' => 'JWT', 'alg' => 'none']));
73+
$claims = $encoder->base64UrlEncode($encoder->jsonEncode(['iat' => $issuedAt]));
74+
75+
$config = Configuration::forUnsecuredSigner();
76+
$parsedToken = $config->parser()->parse($headers . '.' . $claims . '.');
77+
78+
self::assertInstanceOf(Plain::class, $parsedToken);
79+
self::assertSame($timeFraction, $parsedToken->claims()->get('iat')->format('U.u'));
80+
}
81+
82+
/** @return iterable<array{0: float|int|string, 1: string}> */
83+
public function timeFractionConversions(): iterable
84+
{
85+
yield [1616481863.528781890869140625, '1616481863.528782'];
86+
yield [1616497608.0510409, '1616497608.051041'];
87+
yield [1616536852.1000001, '1616536852.100000'];
88+
yield [1616457346.3878131, '1616457346.387813'];
89+
yield [1616457346.0, '1616457346.000000'];
90+
91+
yield [1616457346, '1616457346.000000'];
92+
93+
yield ['1616481863.528781890869140625', '1616481863.528782'];
94+
yield ['1616497608.0510409', '1616497608.051041'];
95+
yield ['1616536852.1000001', '1616536852.100000'];
96+
yield ['1616457346.3878131', '1616457346.387813'];
97+
yield ['1616457346.0', '1616457346.000000'];
98+
yield ['1616457346', '1616457346.000000'];
99+
}
61100
}

test/unit/Token/ParserTest.php

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,49 @@ public function parseMustConvertDateClaimsToObjects(): void
485485
);
486486
}
487487

488+
/**
489+
* @test
490+
*
491+
* @covers ::__construct
492+
* @covers ::parse
493+
* @covers ::splitJwt
494+
* @covers ::parseHeader
495+
* @covers ::parseClaims
496+
* @covers ::parseSignature
497+
* @covers ::convertDate
498+
*
499+
* @uses \Lcobucci\JWT\Token\Plain
500+
* @uses \Lcobucci\JWT\Token\Signature
501+
* @uses \Lcobucci\JWT\Token\DataSet
502+
*/
503+
public function parseMustConvertStringDates(): void
504+
{
505+
$data = [RegisteredClaims::NOT_BEFORE => '1486930757.000000'];
506+
507+
$this->decoder->expects(self::exactly(2))
508+
->method('base64UrlDecode')
509+
->withConsecutive(['a'], ['b'])
510+
->willReturnOnConsecutiveCalls('a_dec', 'b_dec');
511+
512+
$this->decoder->expects(self::exactly(2))
513+
->method('jsonDecode')
514+
->withConsecutive(['a_dec'], ['b_dec'])
515+
->willReturnOnConsecutiveCalls(
516+
['typ' => 'JWT', 'alg' => 'HS256'],
517+
$data
518+
);
519+
520+
$token = $this->createParser()->parse('a.b.');
521+
self::assertInstanceOf(Plain::class, $token);
522+
523+
$claims = $token->claims();
524+
525+
self::assertEquals(
526+
DateTimeImmutable::createFromFormat('U.u', '1486930757.000000'),
527+
$claims->get(RegisteredClaims::NOT_BEFORE)
528+
);
529+
}
530+
488531
/**
489532
* @test
490533
*
@@ -522,4 +565,41 @@ public function parseShouldRaiseExceptionOnInvalidDate(): void
522565
$this->expectExceptionMessage('Value is not in the allowed date format: 14/10/2018 10:50:10.10 UTC');
523566
$this->createParser()->parse('a.b.');
524567
}
568+
569+
/**
570+
* @test
571+
*
572+
* @covers ::__construct
573+
* @covers ::parse
574+
* @covers ::splitJwt
575+
* @covers ::parseHeader
576+
* @covers ::parseClaims
577+
* @covers ::parseSignature
578+
* @covers ::convertDate
579+
* @covers \Lcobucci\JWT\Token\InvalidTokenStructure
580+
*
581+
* @uses \Lcobucci\JWT\Token\Plain
582+
* @uses \Lcobucci\JWT\Token\Signature
583+
* @uses \Lcobucci\JWT\Token\DataSet
584+
*/
585+
public function parseShouldRaiseExceptionOnTimestampBeyondDateTimeImmutableRange(): void
586+
{
587+
$data = [RegisteredClaims::ISSUED_AT => -10000000000 ** 5];
588+
589+
$this->decoder->expects(self::exactly(2))
590+
->method('base64UrlDecode')
591+
->withConsecutive(['a'], ['b'])
592+
->willReturnOnConsecutiveCalls('a_dec', 'b_dec');
593+
594+
$this->decoder->expects(self::exactly(2))
595+
->method('jsonDecode')
596+
->withConsecutive(['a_dec'], ['b_dec'])
597+
->willReturnOnConsecutiveCalls(
598+
['typ' => 'JWT', 'alg' => 'HS256'],
599+
$data
600+
);
601+
602+
$this->expectException(InvalidTokenStructure::class);
603+
$this->createParser()->parse('a.b.');
604+
}
525605
}

0 commit comments

Comments
 (0)