Skip to content

Commit 89edbc5

Browse files
committed
Support psr/clock
1 parent 1945126 commit 89edbc5

9 files changed

+73
-33
lines changed

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,13 @@
5959
"ext-sodium": "*",
6060
"brick/math": "^0.9|^0.10|^0.11",
6161
"paragonie/constant_time_encoding": "^2.4",
62+
"psr/clock": "^1.0",
6263
"psr/event-dispatcher": "^1.0",
6364
"psr/http-client": "^1.0",
6465
"psr/http-factory": "^1.0",
6566
"spomky-labs/aes-key-wrap": "^7.0",
6667
"spomky-labs/pki-framework": "^1.0",
68+
"symfony/clock": "^6.2",
6769
"symfony/config": "^5.4|^6.0",
6870
"symfony/console": "^5.4|^6.0",
6971
"symfony/dependency-injection": "^5.4|^6.0",

src/Component/Checker/ExpirationTimeChecker.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace Jose\Component\Checker;
66

7+
use Psr\Clock\ClockInterface;
8+
use Symfony\Component\Clock\NativeClock;
79
use function is_float;
810
use function is_int;
911

@@ -16,7 +18,8 @@ final class ExpirationTimeChecker implements ClaimChecker, HeaderChecker
1618

1719
public function __construct(
1820
private readonly int $allowedTimeDrift = 0,
19-
private readonly bool $protectedHeaderOnly = false
21+
private readonly bool $protectedHeaderOnly = false,
22+
private readonly ClockInterface $clock = new NativeClock(),
2023
) {
2124
}
2225

@@ -28,7 +31,9 @@ public function checkClaim(mixed $value): void
2831
if (! is_float($value) && ! is_int($value)) {
2932
throw new InvalidClaimException('"exp" must be an integer.', self::NAME, $value);
3033
}
31-
if (time() > $value + $this->allowedTimeDrift) {
34+
35+
$now = $this->clock->now()->getTimestamp();
36+
if ($now > $value + $this->allowedTimeDrift) {
3237
throw new InvalidClaimException('The token expired.', self::NAME, $value);
3338
}
3439
}
@@ -43,7 +48,9 @@ public function checkHeader(mixed $value): void
4348
if (! is_float($value) && ! is_int($value)) {
4449
throw new InvalidHeaderException('"exp" must be an integer.', self::NAME, $value);
4550
}
46-
if (time() > $value + $this->allowedTimeDrift) {
51+
52+
$now = $this->clock->now()->getTimestamp();
53+
if ($now > $value + $this->allowedTimeDrift) {
4754
throw new InvalidHeaderException('The token expired.', self::NAME, $value);
4855
}
4956
}

src/Component/Checker/IssuedAtChecker.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace Jose\Component\Checker;
66

7+
use Psr\Clock\ClockInterface;
8+
use Symfony\Component\Clock\NativeClock;
79
use function is_float;
810
use function is_int;
911

@@ -16,7 +18,8 @@ final class IssuedAtChecker implements ClaimChecker, HeaderChecker
1618

1719
public function __construct(
1820
private readonly int $allowedTimeDrift = 0,
19-
private readonly bool $protectedHeaderOnly = false
21+
private readonly bool $protectedHeaderOnly = false,
22+
private readonly ClockInterface $clock = new NativeClock(),
2023
) {
2124
}
2225

@@ -28,7 +31,9 @@ public function checkClaim(mixed $value): void
2831
if (! is_float($value) && ! is_int($value)) {
2932
throw new InvalidClaimException('"iat" must be an integer.', self::NAME, $value);
3033
}
31-
if (time() < $value - $this->allowedTimeDrift) {
34+
35+
$now = $this->clock->now()->getTimestamp();
36+
if ($now < $value - $this->allowedTimeDrift) {
3237
throw new InvalidClaimException('The JWT is issued in the future.', self::NAME, $value);
3338
}
3439
}
@@ -43,7 +48,9 @@ public function checkHeader(mixed $value): void
4348
if (! is_float($value) && ! is_int($value)) {
4449
throw new InvalidHeaderException('The header "iat" must be an integer.', self::NAME, $value);
4550
}
46-
if (time() < $value - $this->allowedTimeDrift) {
51+
52+
$now = $this->clock->now()->getTimestamp();
53+
if ($now < $value - $this->allowedTimeDrift) {
4754
throw new InvalidHeaderException('The JWT is issued in the future.', self::NAME, $value);
4855
}
4956
}

src/Component/Checker/NotBeforeChecker.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace Jose\Component\Checker;
66

7+
use Psr\Clock\ClockInterface;
8+
use Symfony\Component\Clock\NativeClock;
79
use function is_float;
810
use function is_int;
911

@@ -16,7 +18,8 @@ final class NotBeforeChecker implements ClaimChecker, HeaderChecker
1618

1719
public function __construct(
1820
private readonly int $allowedTimeDrift = 0,
19-
private readonly bool $protectedHeaderOnly = false
21+
private readonly bool $protectedHeaderOnly = false,
22+
private readonly ClockInterface $clock = new NativeClock(),
2023
) {
2124
}
2225

@@ -28,7 +31,9 @@ public function checkClaim(mixed $value): void
2831
if (! is_float($value) && ! is_int($value)) {
2932
throw new InvalidClaimException('"nbf" must be an integer.', self::NAME, $value);
3033
}
31-
if (time() < $value - $this->allowedTimeDrift) {
34+
35+
$now = $this->clock->now()->getTimestamp();
36+
if ($now < $value - $this->allowedTimeDrift) {
3237
throw new InvalidClaimException('The JWT can not be used yet.', self::NAME, $value);
3338
}
3439
}
@@ -43,7 +48,9 @@ public function checkHeader(mixed $value): void
4348
if (! is_float($value) && ! is_int($value)) {
4449
throw new InvalidHeaderException('"nbf" must be an integer.', self::NAME, $value);
4550
}
46-
if (time() < $value - $this->allowedTimeDrift) {
51+
52+
$now = $this->clock->now()->getTimestamp();
53+
if ($now < $value - $this->allowedTimeDrift) {
4754
throw new InvalidHeaderException('The JWT can not be used yet.', self::NAME, $value);
4855
}
4956
}

src/Component/Checker/composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
},
4040
"require": {
4141
"php": ">=8.1",
42+
"psr/clock": "^1.0",
43+
"symfony/clock": "^6.2",
4244
"web-token/jwt-core": "^3.0"
4345
}
4446
}

tests/Component/Checker/ClaimCheckerManagerFactoryTest.php

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
use Jose\Component\Checker\MissingMandatoryClaimException;
1313
use Jose\Component\Checker\NotBeforeChecker;
1414
use PHPUnit\Framework\TestCase;
15+
use Psr\Clock\ClockInterface;
16+
use Symfony\Component\Clock\MockClock;
1517

1618
/**
1719
* @internal
@@ -55,15 +57,17 @@ public function iCanCreateAClaimCheckerManager(): void
5557
*/
5658
public function iCanCheckValidPayloadClaims(): void
5759
{
60+
$clock = new MockClock();
61+
$now = $clock->now()->getTimestamp();
5862
$payload = [
59-
'exp' => time() + 3600,
60-
'iat' => time() - 1000,
61-
'nbf' => time() - 100,
63+
'exp' => $now + 3600,
64+
'iat' => $now - 1000,
65+
'nbf' => $now - 100,
6266
'foo' => 'bar',
6367
];
6468
$expected = $payload;
6569
unset($expected['foo']);
66-
$manager = $this->getClaimCheckerManagerFactory()
70+
$manager = $this->getClaimCheckerManagerFactory($clock)
6771
->create(['exp', 'iat', 'nbf', 'aud']);
6872
$result = $manager->check($payload);
6973
static::assertSame($expected, $result);
@@ -77,26 +81,28 @@ public function theMandatoryClaimsAreNotSet(): void
7781
$this->expectException(MissingMandatoryClaimException::class);
7882
$this->expectExceptionMessage('The following claims are mandatory: bar.');
7983

84+
$clock = new MockClock();
85+
$now = $clock->now()->getTimestamp();
8086
$payload = [
81-
'exp' => time() + 3600,
82-
'iat' => time() - 1000,
83-
'nbf' => time() - 100,
87+
'exp' => $now + 3600,
88+
'iat' => $now - 1000,
89+
'nbf' => $now - 100,
8490
'foo' => 'bar',
8591
];
8692
$expected = $payload;
8793
unset($expected['foo']);
88-
$manager = $this->getClaimCheckerManagerFactory()
94+
$manager = $this->getClaimCheckerManagerFactory($clock)
8995
->create(['exp', 'iat', 'nbf', 'aud']);
9096
$manager->check($payload, ['exp', 'foo', 'bar']);
9197
}
9298

93-
private function getClaimCheckerManagerFactory(): ClaimCheckerManagerFactory
99+
private function getClaimCheckerManagerFactory(ClockInterface $clock = new MockClock()): ClaimCheckerManagerFactory
94100
{
95101
if ($this->claimCheckerManagerFactory === null) {
96102
$this->claimCheckerManagerFactory = new ClaimCheckerManagerFactory();
97-
$this->claimCheckerManagerFactory->add('exp', new ExpirationTimeChecker());
98-
$this->claimCheckerManagerFactory->add('iat', new IssuedAtChecker());
99-
$this->claimCheckerManagerFactory->add('nbf', new NotBeforeChecker());
103+
$this->claimCheckerManagerFactory->add('exp', new ExpirationTimeChecker(clock: $clock));
104+
$this->claimCheckerManagerFactory->add('iat', new IssuedAtChecker(clock: $clock));
105+
$this->claimCheckerManagerFactory->add('nbf', new NotBeforeChecker(clock: $clock));
100106
$this->claimCheckerManagerFactory->add('aud', new AudienceChecker('My Service'));
101107
}
102108

tests/Component/Checker/ExpirationTimeClaimCheckerTest.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Jose\Component\Checker\ExpirationTimeChecker;
88
use Jose\Component\Checker\InvalidClaimException;
99
use PHPUnit\Framework\TestCase;
10+
use Symfony\Component\Clock\MockClock;
1011

1112
/**
1213
* @internal
@@ -33,17 +34,19 @@ public function theExpirationTimeIsInThePast(): void
3334
$this->expectException(InvalidClaimException::class);
3435
$this->expectExceptionMessage('The token expired.');
3536

36-
$checker = new ExpirationTimeChecker();
37-
$checker->checkClaim(time() - 1);
37+
$clock = new MockClock();
38+
$checker = new ExpirationTimeChecker(clock: $clock);
39+
$checker->checkClaim($clock->now()->getTimestamp() - 1);
3840
}
3941

4042
/**
4143
* @test
4244
*/
4345
public function theExpirationTimeIsInTheFutur(): void
4446
{
45-
$checker = new ExpirationTimeChecker();
46-
$checker->checkClaim(time() + 3600);
47+
$clock = new MockClock();
48+
$checker = new ExpirationTimeChecker(clock: $clock);
49+
$checker->checkClaim($clock->now()->getTimestamp() + 3600);
4750
static::assertSame('exp', $checker->supportedClaim());
4851
}
4952
}

tests/Component/Checker/IssuedAtClaimCheckerTest.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Jose\Component\Checker\InvalidClaimException;
88
use Jose\Component\Checker\IssuedAtChecker;
99
use PHPUnit\Framework\TestCase;
10+
use Symfony\Component\Clock\MockClock;
1011

1112
/**
1213
* @internal
@@ -33,17 +34,19 @@ public function theIssuedAtClaimIsInTheFutur(): void
3334
$this->expectException(InvalidClaimException::class);
3435
$this->expectExceptionMessage('The JWT is issued in the future.');
3536

36-
$checker = new IssuedAtChecker();
37-
$checker->checkClaim(time() + 3600);
37+
$clock = new MockClock();
38+
$checker = new IssuedAtChecker(clock: $clock);
39+
$checker->checkClaim($clock->now()->getTimestamp() + 3600);
3840
}
3941

4042
/**
4143
* @test
4244
*/
4345
public function theIssuedAtClaimIsInThePast(): void
4446
{
45-
$checker = new IssuedAtChecker();
46-
$checker->checkClaim(time() - 3600);
47+
$clock = new MockClock();
48+
$checker = new IssuedAtChecker(clock: $clock);
49+
$checker->checkClaim($clock->now()->getTimestamp() - 3600);
4750
static::assertSame('iat', $checker->supportedClaim());
4851
}
4952
}

tests/Component/Checker/NotBeforeClaimCheckerTest.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Jose\Component\Checker\InvalidClaimException;
88
use Jose\Component\Checker\NotBeforeChecker;
99
use PHPUnit\Framework\TestCase;
10+
use Symfony\Component\Clock\MockClock;
1011

1112
/**
1213
* @internal
@@ -33,17 +34,19 @@ public function theNotBeforeClaimIsInTheFutur(): void
3334
$this->expectException(InvalidClaimException::class);
3435
$this->expectExceptionMessage('The JWT can not be used yet.');
3536

36-
$checker = new NotBeforeChecker();
37-
$checker->checkClaim(time() + 3600);
37+
$clock = new MockClock();
38+
$checker = new NotBeforeChecker(clock: $clock);
39+
$checker->checkClaim($clock->now()->getTimestamp() + 3600);
3840
}
3941

4042
/**
4143
* @test
4244
*/
4345
public function theNotBeforeClaimIsInThePast(): void
4446
{
45-
$checker = new NotBeforeChecker();
46-
$checker->checkClaim(time() - 3600);
47+
$clock = new MockClock();
48+
$checker = new NotBeforeChecker(clock: $clock);
49+
$checker->checkClaim($clock->now()->getTimestamp() - 3600);
4750
static::assertSame('nbf', $checker->supportedClaim());
4851
}
4952
}

0 commit comments

Comments
 (0)