From 81dba8454d739328ec501ceb361e5b2900a0f20b Mon Sep 17 00:00:00 2001 From: Robin Nydahl Date: Wed, 30 Oct 2024 18:06:53 +0100 Subject: [PATCH 1/2] Implement config variable to allow iat to remain unchanged claim when refreshing a token --- CHANGELOG.md | 1 + config/config.php | 19 +++++++++--- src/Manager.php | 23 +++++++++++++- src/Providers/AbstractServiceProvider.php | 1 + tests/DefaultConfigValuesTest.php | 5 +++ tests/ManagerTest.php | 38 +++++++++++++++++++++++ 6 files changed, 82 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9535882a..efbc24b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ You can find and compare releases at the GitHub release page. ## [Unreleased] ### Added +- #268 Implement config variable to allow iat to remain unchanged claim when refreshing a token - Fixes #259 - Can't logout with an expired token - Add `cookie_key_name` config to customize cookie name for authentication diff --git a/config/config.php b/config/config.php index 865c028a..126de710 100644 --- a/config/config.php +++ b/config/config.php @@ -96,10 +96,20 @@ | Refresh time to live |-------------------------------------------------------------------------- | - | Specify the length of time (in minutes) that the token can be refreshed - | within. I.E. The user can refresh their token within a 2 week window of - | the original token being created until they must re-authenticate. - | Defaults to 2 weeks. + | Specify the length of time (in minutes) that the token can be refreshed within. + | This defines the refresh window, during which the user can refresh their token + | before re-authentication is required. + | + | By default, each refresh will issue a new "iat" (issued at) timestamp, extending + | the refresh period from the most recent refresh. This results in a rolling refresh + | expiry, where the refresh window resets with each token refresh. + | + | To retain a fixed refresh window from the original token creation (i.e., the behavior + | prior to version 2.5.0), set "refresh_iat" to false. With this setting, the refresh + | window will remain based on the original "iat" of the initial token issued, regardless + | of subsequent refreshes. + | + | The refresh ttl defaults to 2 weeks. | | You can also set this to null, to yield an infinite refresh time. | Some may want this instead of never expiring tokens for e.g. a mobile app. @@ -108,6 +118,7 @@ | */ + 'refresh_iat' => env('JWT_REFRESH_IAT', true), 'refresh_ttl' => env('JWT_REFRESH_TTL', 20160), /* diff --git a/src/Manager.php b/src/Manager.php index f584a0ca..46067fdf 100644 --- a/src/Manager.php +++ b/src/Manager.php @@ -52,6 +52,13 @@ class Manager */ protected $blacklistEnabled = true; + /** + * The refresh iat flag. + * + * @var bool + */ + protected $refreshIat = true; + /** * the persistent claims. * @@ -182,7 +189,7 @@ protected function buildRefreshClaims(Payload $payload) $persistentClaims, [ 'sub' => $payload['sub'], - 'iat' => Utils::now()->timestamp, + 'iat' => $this->refreshIat ? Utils::now()->timestamp : $payload['iat'], ] ); } @@ -267,4 +274,18 @@ public function setPersistentClaims(array $claims) return $this; } + + /** + * Set whether the refresh iat is enabled. + * + * @param bool $enabled + * + * @return $this + */ + public function setRefreshIat($refreshIat) + { + $this->refreshIat = $refreshIat; + + return $this; + } } diff --git a/src/Providers/AbstractServiceProvider.php b/src/Providers/AbstractServiceProvider.php index c1d730fa..6a004256 100644 --- a/src/Providers/AbstractServiceProvider.php +++ b/src/Providers/AbstractServiceProvider.php @@ -213,6 +213,7 @@ protected function registerManager() ); return $instance->setBlacklistEnabled((bool) $app->make('config')->get('jwt.blacklist_enabled')) + ->setRefreshIat((bool) $app->make('config')->get('jwt.refresh_iat', true)) ->setPersistentClaims($app->make('config')->get('jwt.persistent_claims')) ->setBlackListExceptionEnabled((bool) $app->make('config')->get('jwt.show_black_list_exception', 0)); }); diff --git a/tests/DefaultConfigValuesTest.php b/tests/DefaultConfigValuesTest.php index 40dd94ae..ed3d85fa 100644 --- a/tests/DefaultConfigValuesTest.php +++ b/tests/DefaultConfigValuesTest.php @@ -44,6 +44,11 @@ public function testTtlShouldBeSet() $this->assertEquals(60, $this->configuration['ttl']); } + public function testRefreshIatShouldBeSet() + { + $this->assertEquals(true, $this->configuration['refresh_iat']); + } + public function testRefreshTtlShouldBeSet() { $this->assertEquals(20160, $this->configuration['refresh_ttl']); diff --git a/tests/ManagerTest.php b/tests/ManagerTest.php index e9c6ab85..d6a9be84 100644 --- a/tests/ManagerTest.php +++ b/tests/ManagerTest.php @@ -220,6 +220,44 @@ public function testBuildRefreshClaimsMethodWillRefreshTheIAT() $this->assertNotEquals($firstResult['iat'], $secondResult['iat']); } + public function testBuildRefreshClaimsMethodWillNotRefreshTheIAT() + { + $this->app['config']->set('jwt.refresh_iat', false); + + $claims = [ + new Subject(1), + new Issuer('http://example.com'), + new Expiration($this->testNowTimestamp - 3600), + new NotBefore($this->testNowTimestamp), + new IssuedAt($this->testNowTimestamp), + new JwtId('foo'), + ]; + $collection = Collection::make($claims); + + $this->validator->shouldReceive('setRefreshFlow->check')->andReturn($collection); + $payload = new Payload($collection, $this->validator); + + $managerClass = new \ReflectionClass(Manager::class); + $buildRefreshClaimsMethod = $managerClass->getMethod('buildRefreshClaims'); + $buildRefreshClaimsMethod->setAccessible(true); + $managerInstance = new Manager($this->jwt, $this->blacklist, $this->factory); + + $firstResult = $buildRefreshClaimsMethod->invokeArgs($managerInstance, [$payload]); + Carbon::setTestNow(Carbon::now()->addMinutes(2)); + $secondResult = $buildRefreshClaimsMethod->invokeArgs($managerInstance, [$payload]); + + $this->assertIsInt($firstResult['iat']); + $this->assertIsInt($secondResult['iat']); + + $carbonTimestamp = Carbon::createFromTimestamp($firstResult['iat']); + $this->assertInstanceOf(Carbon::class, $carbonTimestamp); + + $carbonTimestamp = Carbon::createFromTimestamp($secondResult['iat']); + $this->assertInstanceOf(Carbon::class, $carbonTimestamp); + + $this->assertEquals($firstResult['iat'], $secondResult['iat']); + } + /** * @throws InvalidClaimException */ From 9d8bb9592a104c7de0410dc6edc5e104f395b11b Mon Sep 17 00:00:00 2001 From: Robin Nydahl Date: Wed, 30 Oct 2024 18:21:14 +0100 Subject: [PATCH 2/2] Fix bug in Manager Test --- tests/ManagerTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/ManagerTest.php b/tests/ManagerTest.php index d6a9be84..a09a2cd6 100644 --- a/tests/ManagerTest.php +++ b/tests/ManagerTest.php @@ -222,8 +222,6 @@ public function testBuildRefreshClaimsMethodWillRefreshTheIAT() public function testBuildRefreshClaimsMethodWillNotRefreshTheIAT() { - $this->app['config']->set('jwt.refresh_iat', false); - $claims = [ new Subject(1), new Issuer('http://example.com'), @@ -241,6 +239,7 @@ public function testBuildRefreshClaimsMethodWillNotRefreshTheIAT() $buildRefreshClaimsMethod = $managerClass->getMethod('buildRefreshClaims'); $buildRefreshClaimsMethod->setAccessible(true); $managerInstance = new Manager($this->jwt, $this->blacklist, $this->factory); + $managerInstance->setRefreshIat(false); $firstResult = $buildRefreshClaimsMethod->invokeArgs($managerInstance, [$payload]); Carbon::setTestNow(Carbon::now()->addMinutes(2));