From 81dba8454d739328ec501ceb361e5b2900a0f20b Mon Sep 17 00:00:00 2001 From: Robin Nydahl Date: Wed, 30 Oct 2024 18:06:53 +0100 Subject: [PATCH 1/4] 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/4] 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)); From ddc0fe5234c3dd07df0ce61d179232c1d5d74b71 Mon Sep 17 00:00:00 2001 From: Robin Nydahl Date: Thu, 3 Jul 2025 12:20:03 +0200 Subject: [PATCH 3/4] Change Refresh Issued At Flag to be false by default --- config/config.php | 2 +- src/Manager.php | 2 +- src/Providers/AbstractServiceProvider.php | 2 +- tests/.ManagerTest.php.swp | Bin 0 -> 24576 bytes tests/DefaultConfigValuesTest.php | 2 +- tests/ManagerTest.php | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 tests/.ManagerTest.php.swp diff --git a/config/config.php b/config/config.php index 1cafac82..1f7cb95d 100644 --- a/config/config.php +++ b/config/config.php @@ -118,7 +118,7 @@ | */ - 'refresh_iat' => env('JWT_REFRESH_IAT', true), + 'refresh_iat' => env('JWT_REFRESH_IAT', false), 'refresh_ttl' => (int) env('JWT_REFRESH_TTL', 20160), /* diff --git a/src/Manager.php b/src/Manager.php index 46067fdf..5d3a0224 100644 --- a/src/Manager.php +++ b/src/Manager.php @@ -57,7 +57,7 @@ class Manager * * @var bool */ - protected $refreshIat = true; + protected $refreshIat = false; /** * the persistent claims. diff --git a/src/Providers/AbstractServiceProvider.php b/src/Providers/AbstractServiceProvider.php index 6a004256..45899ce6 100644 --- a/src/Providers/AbstractServiceProvider.php +++ b/src/Providers/AbstractServiceProvider.php @@ -213,7 +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)) + ->setRefreshIat((bool) $app->make('config')->get('jwt.refresh_iat', false)) ->setPersistentClaims($app->make('config')->get('jwt.persistent_claims')) ->setBlackListExceptionEnabled((bool) $app->make('config')->get('jwt.show_black_list_exception', 0)); }); diff --git a/tests/.ManagerTest.php.swp b/tests/.ManagerTest.php.swp new file mode 100644 index 0000000000000000000000000000000000000000..571f5241c133660acaafab694eda7c3b31cbbc75 GIT binary patch literal 24576 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 literal 0 HcmV?d00001 diff --git a/tests/DefaultConfigValuesTest.php b/tests/DefaultConfigValuesTest.php index ed3d85fa..a59f2cca 100644 --- a/tests/DefaultConfigValuesTest.php +++ b/tests/DefaultConfigValuesTest.php @@ -46,7 +46,7 @@ public function testTtlShouldBeSet() public function testRefreshIatShouldBeSet() { - $this->assertEquals(true, $this->configuration['refresh_iat']); + $this->assertEquals(false, $this->configuration['refresh_iat']); } public function testRefreshTtlShouldBeSet() diff --git a/tests/ManagerTest.php b/tests/ManagerTest.php index a09a2cd6..b9175c10 100644 --- a/tests/ManagerTest.php +++ b/tests/ManagerTest.php @@ -203,6 +203,7 @@ public function testBuildRefreshClaimsMethodWillRefreshTheIAT() $buildRefreshClaimsMethod = $managerClass->getMethod('buildRefreshClaims'); $buildRefreshClaimsMethod->setAccessible(true); $managerInstance = new Manager($this->jwt, $this->blacklist, $this->factory); + $managerInstance->setRefreshIat(true); $firstResult = $buildRefreshClaimsMethod->invokeArgs($managerInstance, [$payload]); Carbon::setTestNow(Carbon::now()->addMinutes(2)); @@ -239,7 +240,6 @@ 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)); From 0a1c22159604cf6d2e4a323911ff84171f877eb3 Mon Sep 17 00:00:00 2001 From: Robin Nydahl Date: Thu, 3 Jul 2025 12:38:52 +0200 Subject: [PATCH 4/4] Update config description --- config/config.php | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/config/config.php b/config/config.php index 1f7cb95d..5f817b7d 100644 --- a/config/config.php +++ b/config/config.php @@ -100,14 +100,13 @@ | 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. + | By default, a refresh will NOT issue a new "iat" (issued at) timestamp. If changed + | to true, each refresh will issue a new "iat" timestamp, extending the refresh + | period from the most recent refresh. This results in a rolling refresh + | + | To retain a fluid refresh window from the last refresh action (i.e., the behavior between + | version 2.5.0 and 2.8.2), set "refresh_iat" to true. With this setting, the refresh + | window will renew with each subsequent refresh. | | The refresh ttl defaults to 2 weeks. |