Skip to content

Commit f01e77f

Browse files
tscniSpomky
andauthored
Support PSR-20 (clock) (#433)
* Support psr/clock * Deprecate not proving a clock implementation * Clock for the bundle --------- Co-authored-by: Florent Morselli <florent.morselli@spomky-labs.com>
1 parent 1945126 commit f01e77f

File tree

14 files changed

+182
-39
lines changed

14 files changed

+182
-39
lines changed

composer.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
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",
@@ -145,7 +146,9 @@
145146
"sort-packages": true,
146147
"allow-plugins": {
147148
"infection/extension-installer": true,
148-
"composer/package-versions-deprecated": true
149+
"composer/package-versions-deprecated": true,
150+
"phpstan/extension-installer": false,
151+
"php-http/discovery": false
149152
}
150153
}
151154
}

rector.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
DoctrineSetList::DOCTRINE_CODE_QUALITY,
2323
DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES,
2424
PHPUnitSetList::PHPUNIT_SPECIFIC_METHOD,
25-
PHPUnitLevelSetList::UP_TO_PHPUNIT_100,
25+
PHPUnitLevelSetList::UP_TO_PHPUNIT_90,
2626
PHPUnitSetList::PHPUNIT_CODE_QUALITY,
2727
PHPUnitSetList::PHPUNIT_EXCEPTION,
2828
PHPUnitSetList::REMOVE_MOCKS,

src/Bundle/JoseFramework/DependencyInjection/Source/Checker/CheckerSource.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public function load(array $configs, ContainerBuilder $container): void
4545
$loader = new PhpFileLoader($container, new FileLocator(__DIR__ . '/../../../Resources/config'));
4646
$loader->load('checkers.php');
4747

48+
$container->setAlias('jose.clock', $configs['clock']);
4849
if (array_key_exists('checkers', $configs)) {
4950
foreach ($this->sources as $source) {
5051
$source->load($configs['checkers'], $container);
@@ -57,6 +58,13 @@ public function getNodeDefinition(NodeDefinition $node): void
5758
if (! $this->isEnabled()) {
5859
return;
5960
}
61+
$node->children()
62+
->scalarNode('clock')
63+
->defaultValue('jose.internal_clock')
64+
->cannotBeEmpty()
65+
->info('PSR-20 clock')
66+
->end()
67+
->end();
6068
$childNode = $node
6169
->children()
6270
->arrayNode($this->name())

src/Bundle/JoseFramework/Resources/config/checkers.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
use Jose\Bundle\JoseFramework\Services\ClaimCheckerManagerFactory;
66
use Jose\Bundle\JoseFramework\Services\HeaderCheckerManagerFactory;
77
use Jose\Component\Checker\ExpirationTimeChecker;
8+
use Jose\Component\Checker\InternalClock;
89
use Jose\Component\Checker\IssuedAtChecker;
910
use Jose\Component\Checker\NotBeforeChecker;
11+
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
12+
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
1013

1114
/*
1215
* The MIT License (MIT)
@@ -17,7 +20,6 @@
1720
* of the MIT license. See the LICENSE file for details.
1821
*/
1922

20-
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
2123

2224
return function (ContainerConfigurator $container): void {
2325
$container = $container->services()
@@ -33,6 +35,7 @@
3335
->public();
3436

3537
$container->set(ExpirationTimeChecker::class)
38+
->arg('$clock', service('jose.internal_clock'))
3639
->tag('jose.checker.claim', [
3740
'alias' => 'exp',
3841
])
@@ -41,6 +44,7 @@
4144
]);
4245

4346
$container->set(IssuedAtChecker::class)
47+
->arg('$clock', service('jose.internal_clock'))
4448
->tag('jose.checker.claim', [
4549
'alias' => 'iat',
4650
])
@@ -49,10 +53,20 @@
4953
]);
5054

5155
$container->set(NotBeforeChecker::class)
56+
->arg('$clock', service('jose.internal_clock'))
5257
->tag('jose.checker.claim', [
5358
'alias' => 'nbf',
5459
])
5560
->tag('jose.checker.header', [
5661
'alias' => 'nbf',
5762
]);
63+
64+
$container->set('jose.internal_clock')
65+
->class(InternalClock::class)
66+
->deprecate(
67+
'web-token/jwt-bundle',
68+
'3.2.0',
69+
'The service "%service_id%" is an internal service that will be removed in 4.0.0. Please use a PSR-20 compatible service as clock.'
70+
)
71+
->private();
5872
};

src/Component/Checker/ExpirationTimeChecker.php

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use function is_float;
88
use function is_int;
9+
use Psr\Clock\ClockInterface;
910

1011
/**
1112
* This class is a claim checker. When the "exp" is present, it will compare the value with the current timestamp.
@@ -14,10 +15,22 @@ final class ExpirationTimeChecker implements ClaimChecker, HeaderChecker
1415
{
1516
private const NAME = 'exp';
1617

18+
private readonly ClockInterface $clock;
19+
1720
public function __construct(
1821
private readonly int $allowedTimeDrift = 0,
19-
private readonly bool $protectedHeaderOnly = false
22+
private readonly bool $protectedHeaderOnly = false,
23+
?ClockInterface $clock = null,
2024
) {
25+
if ($clock === null) {
26+
trigger_deprecation(
27+
'web-token/jwt-checker',
28+
'3.2.0',
29+
'The parameter "$clock" will become mandatory in 4.0.0. Please set a valid PSR Clock implementation instead of "null".'
30+
);
31+
$clock = new InternalClock();
32+
}
33+
$this->clock = $clock;
2134
}
2235

2336
/**
@@ -28,7 +41,10 @@ public function checkClaim(mixed $value): void
2841
if (! is_float($value) && ! is_int($value)) {
2942
throw new InvalidClaimException('"exp" must be an integer.', self::NAME, $value);
3043
}
31-
if (time() > $value + $this->allowedTimeDrift) {
44+
45+
$now = $this->clock->now()
46+
->getTimestamp();
47+
if ($now > $value + $this->allowedTimeDrift) {
3248
throw new InvalidClaimException('The token expired.', self::NAME, $value);
3349
}
3450
}
@@ -43,7 +59,10 @@ public function checkHeader(mixed $value): void
4359
if (! is_float($value) && ! is_int($value)) {
4460
throw new InvalidHeaderException('"exp" must be an integer.', self::NAME, $value);
4561
}
46-
if (time() > $value + $this->allowedTimeDrift) {
62+
63+
$now = $this->clock->now()
64+
->getTimestamp();
65+
if ($now > $value + $this->allowedTimeDrift) {
4766
throw new InvalidHeaderException('The token expired.', self::NAME, $value);
4867
}
4968
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Jose\Component\Checker;
6+
7+
use DateTimeImmutable;
8+
use Psr\Clock\ClockInterface;
9+
10+
/**
11+
* @internal
12+
*/
13+
final class InternalClock implements ClockInterface
14+
{
15+
public function now(): DateTimeImmutable
16+
{
17+
return new DateTimeImmutable();
18+
}
19+
}

src/Component/Checker/IssuedAtChecker.php

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use function is_float;
88
use function is_int;
9+
use Psr\Clock\ClockInterface;
910

1011
/**
1112
* This class is a claim checker. When the "iat" is present, it will compare the value with the current timestamp.
@@ -14,10 +15,22 @@ final class IssuedAtChecker implements ClaimChecker, HeaderChecker
1415
{
1516
private const NAME = 'iat';
1617

18+
private readonly ClockInterface $clock;
19+
1720
public function __construct(
1821
private readonly int $allowedTimeDrift = 0,
19-
private readonly bool $protectedHeaderOnly = false
22+
private readonly bool $protectedHeaderOnly = false,
23+
?ClockInterface $clock = null,
2024
) {
25+
if ($clock === null) {
26+
trigger_deprecation(
27+
'web-token/jwt-checker',
28+
'3.2.0',
29+
'The parameter "$clock" will become mandatory in 4.0.0. Please set a valid PSR Clock implementation instead of "null".'
30+
);
31+
$clock = new InternalClock();
32+
}
33+
$this->clock = $clock;
2134
}
2235

2336
/**
@@ -28,7 +41,10 @@ public function checkClaim(mixed $value): void
2841
if (! is_float($value) && ! is_int($value)) {
2942
throw new InvalidClaimException('"iat" must be an integer.', self::NAME, $value);
3043
}
31-
if (time() < $value - $this->allowedTimeDrift) {
44+
45+
$now = $this->clock->now()
46+
->getTimestamp();
47+
if ($now < $value - $this->allowedTimeDrift) {
3248
throw new InvalidClaimException('The JWT is issued in the future.', self::NAME, $value);
3349
}
3450
}
@@ -43,7 +59,10 @@ public function checkHeader(mixed $value): void
4359
if (! is_float($value) && ! is_int($value)) {
4460
throw new InvalidHeaderException('The header "iat" must be an integer.', self::NAME, $value);
4561
}
46-
if (time() < $value - $this->allowedTimeDrift) {
62+
63+
$now = $this->clock->now()
64+
->getTimestamp();
65+
if ($now < $value - $this->allowedTimeDrift) {
4766
throw new InvalidHeaderException('The JWT is issued in the future.', self::NAME, $value);
4867
}
4968
}

src/Component/Checker/NotBeforeChecker.php

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use function is_float;
88
use function is_int;
9+
use Psr\Clock\ClockInterface;
910

1011
/**
1112
* This class is a claim checker. When the "nbf" is present, it will compare the value with the current timestamp.
@@ -14,10 +15,22 @@ final class NotBeforeChecker implements ClaimChecker, HeaderChecker
1415
{
1516
private const NAME = 'nbf';
1617

18+
private readonly ClockInterface $clock;
19+
1720
public function __construct(
1821
private readonly int $allowedTimeDrift = 0,
19-
private readonly bool $protectedHeaderOnly = false
22+
private readonly bool $protectedHeaderOnly = false,
23+
?ClockInterface $clock = null,
2024
) {
25+
if ($clock === null) {
26+
trigger_deprecation(
27+
'web-token/jwt-checker',
28+
'3.2.0',
29+
'The parameter "$clock" will become mandatory in 4.0.0. Please set a valid PSR Clock implementation instead of "null".'
30+
);
31+
$clock = new InternalClock();
32+
}
33+
$this->clock = $clock;
2134
}
2235

2336
/**
@@ -28,7 +41,10 @@ public function checkClaim(mixed $value): void
2841
if (! is_float($value) && ! is_int($value)) {
2942
throw new InvalidClaimException('"nbf" must be an integer.', self::NAME, $value);
3043
}
31-
if (time() < $value - $this->allowedTimeDrift) {
44+
45+
$now = $this->clock->now()
46+
->getTimestamp();
47+
if ($now < $value - $this->allowedTimeDrift) {
3248
throw new InvalidClaimException('The JWT can not be used yet.', self::NAME, $value);
3349
}
3450
}
@@ -43,7 +59,10 @@ public function checkHeader(mixed $value): void
4359
if (! is_float($value) && ! is_int($value)) {
4460
throw new InvalidHeaderException('"nbf" must be an integer.', self::NAME, $value);
4561
}
46-
if (time() < $value - $this->allowedTimeDrift) {
62+
63+
$now = $this->clock->now()
64+
->getTimestamp();
65+
if ($now < $value - $this->allowedTimeDrift) {
4766
throw new InvalidHeaderException('The JWT can not be used yet.', self::NAME, $value);
4867
}
4968
}

src/Component/Checker/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
},
4040
"require": {
4141
"php": ">=8.1",
42+
"psr/clock": "^1.0",
4243
"web-token/jwt-core": "^3.0"
4344
}
4445
}

tests/Component/Checker/ClaimCheckerManagerFactoryTest.php

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
use Jose\Component\Checker\IssuedAtChecker;
1212
use Jose\Component\Checker\MissingMandatoryClaimException;
1313
use Jose\Component\Checker\NotBeforeChecker;
14+
use Jose\Tests\Component\Checker\Stub\MockClock;
1415
use PHPUnit\Framework\TestCase;
16+
use Psr\Clock\ClockInterface;
1517

1618
/**
1719
* @internal
@@ -55,15 +57,18 @@ public function iCanCreateAClaimCheckerManager(): void
5557
*/
5658
public function iCanCheckValidPayloadClaims(): void
5759
{
60+
$clock = new MockClock();
61+
$now = $clock->now()
62+
->getTimestamp();
5863
$payload = [
59-
'exp' => time() + 3600,
60-
'iat' => time() - 1000,
61-
'nbf' => time() - 100,
64+
'exp' => $now + 3600,
65+
'iat' => $now - 1000,
66+
'nbf' => $now - 100,
6267
'foo' => 'bar',
6368
];
6469
$expected = $payload;
6570
unset($expected['foo']);
66-
$manager = $this->getClaimCheckerManagerFactory()
71+
$manager = $this->getClaimCheckerManagerFactory($clock)
6772
->create(['exp', 'iat', 'nbf', 'aud']);
6873
$result = $manager->check($payload);
6974
static::assertSame($expected, $result);
@@ -77,26 +82,29 @@ public function theMandatoryClaimsAreNotSet(): void
7782
$this->expectException(MissingMandatoryClaimException::class);
7883
$this->expectExceptionMessage('The following claims are mandatory: bar.');
7984

85+
$clock = new MockClock();
86+
$now = $clock->now()
87+
->getTimestamp();
8088
$payload = [
81-
'exp' => time() + 3600,
82-
'iat' => time() - 1000,
83-
'nbf' => time() - 100,
89+
'exp' => $now + 3600,
90+
'iat' => $now - 1000,
91+
'nbf' => $now - 100,
8492
'foo' => 'bar',
8593
];
8694
$expected = $payload;
8795
unset($expected['foo']);
88-
$manager = $this->getClaimCheckerManagerFactory()
96+
$manager = $this->getClaimCheckerManagerFactory($clock)
8997
->create(['exp', 'iat', 'nbf', 'aud']);
9098
$manager->check($payload, ['exp', 'foo', 'bar']);
9199
}
92100

93-
private function getClaimCheckerManagerFactory(): ClaimCheckerManagerFactory
101+
private function getClaimCheckerManagerFactory(ClockInterface $clock = new MockClock()): ClaimCheckerManagerFactory
94102
{
95103
if ($this->claimCheckerManagerFactory === null) {
96104
$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());
105+
$this->claimCheckerManagerFactory->add('exp', new ExpirationTimeChecker(clock: $clock));
106+
$this->claimCheckerManagerFactory->add('iat', new IssuedAtChecker(clock: $clock));
107+
$this->claimCheckerManagerFactory->add('nbf', new NotBeforeChecker(clock: $clock));
100108
$this->claimCheckerManagerFactory->add('aud', new AudienceChecker('My Service'));
101109
}
102110

0 commit comments

Comments
 (0)