Skip to content

Commit 568a130

Browse files
authored
feat: Add expiration date to access token & hmac keys (#1219)
* feat: add expiration date to access token & hmac keys/tokens. * fix test assertion * fix test for lower PHP version and PHPDoc blocks * fix token expiration type, tests and docs references. fix invalidateAll hmac cli command. implements CanAccessTokenExpire() and CanHmacTokenExpire() methods. removes hasAccessTokenExpired() and hasHmacTokenExpired() methods. * fix file * fix namespace case. fix hmac invalidateAll() expires field. * fix hasAccessTokenExpired()& hasHmacTokenExpired() args. fix docs refinement. fix test. * fix token expiration args, support remove expiration. fix tokens / hmac docs. fix psalm xml attribute: ensureOverrideAttribute. * fix tokens expiration method names. fix docs and tests. * fix hmac docs warning section * fix hmac expiration docs sample * fix comments and docs * fix test assertion * fix docs and comments. fix updateHmacTokenExpiration() token refresh. * fix tokens expire methods names. fix check() expires type check. updated docs & tests. * fix name case
1 parent 57d5d5c commit 568a130

File tree

13 files changed

+570
-35
lines changed

13 files changed

+570
-35
lines changed

docs/references/authentication/hmac.md

+62-1
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,63 @@ You can revoke all HMAC Keys with the `revokeAllHmacTokens()` method.
9797
$user->revokeAllHmacTokens();
9898
```
9999

100+
## Expiring HMAC Keys
101+
102+
By default, the HMAC keys don't expire unless they reach the HMAC keys' lifetime expiration after their last use date.
103+
104+
HMAC keys can be set to expire through the `generateHmacToken()` method. This takes the expiration date as the `$expiresAt` argument. To update an existing HMAC key expiration date, use `updateHmacTokenExpiration($hmacTokenID, $expiresAt)`. To remove it, use `removeHmacTokenExpiration($hmacTokenID)`.
105+
106+
`$expiresAt` [Time](https://codeigniter.com/user_guide/libraries/time.html) object
107+
108+
```php
109+
// Expiration date = 2024-11-03 12:00:00
110+
$expiresAt = Time::parse('2024-11-03 12:00:00');
111+
$token = $this->user->generateHmacToken('foo', ['foo.bar'], $expiresAt);
112+
113+
// Expiration date = 2024-11-15 00:00:00
114+
$expiresAt = Time::parse('2024-11-15 00:00:00');
115+
$user->updateHmacTokenExpiration($token->id, $expiresAt);
116+
117+
// Expiration date = 1 month + 15 days into the future
118+
$expiresAt = Time::now()->addMonths(1)->addDays(15);
119+
$user->updateHmacTokenExpiration($token->id, $expiresAt);
120+
121+
// Remove the expiration date
122+
$user->removeHmacTokenExpiration($token->id);
123+
```
124+
125+
The following support methods are also available:
126+
127+
`isHmacTokenExpired(AccessToken $hmacToken)` - Checks if the HMAC key is expired. Returns `true` if the HMAC key is expired; otherwise, returns `false`.
128+
129+
```php
130+
$expiresAt = Time::parse('2024-11-03 12:00:00');
131+
$token = $this->user->generateHmacToken('foo', ['foo.bar'], $expiresAt);
132+
133+
$this->user->isHmacTokenExpired($token); // Returns true
134+
```
135+
136+
`canHmacTokenExpire(AccessToken $hmacToken)` - Checks if HMAC key has an expiration set. Returns `true` if the HMAC key is expired; otherwise, returns `false`.
137+
138+
```php
139+
$expiresAt = Time::parse('2024-11-03 12:00:00');
140+
141+
$token = $this->user->generateHmacToken('foo', ['foo.bar'], $expiresAt);
142+
$this->user->canHmacTokenExpire($token); // Returns true
143+
144+
$token2 = $this->user->generateHmacToken('bar');
145+
$this->user->canHmacTokenExpire($token2); // Returns false
146+
```
147+
148+
You can also easily set all existing HMAC keys/tokens as expired with the `spark` command:
149+
```console
150+
php spark shield:hmac invalidateAll
151+
```
152+
153+
!!! warning
154+
155+
This command invalidates _all_ keys for _all_ users.
156+
100157
## Retrieving HMAC Keys
101158

102159
The following methods are available to help you retrieve a user's HMAC keys:
@@ -217,7 +274,7 @@ Configure **app/Config/AuthToken.php** for your needs.
217274

218275
### HMAC Keys Lifetime
219276

220-
HMAC Keys/Tokens will expire after a specified amount of time has passed since they have been used.
277+
HMAC Keys will expire after a specified amount of time has passed since they have been used.
221278

222279
By default, this is set to 1 year. You can change this value by setting the `$unusedTokenLifetime`
223280
value. This is in seconds so that you can use the
@@ -228,6 +285,10 @@ that CodeIgniter provides.
228285
public $unusedTokenLifetime = YEAR;
229286
```
230287

288+
### HMAC Keys Expiration vs Lifetime
289+
290+
Expiration and lifetime are two different concepts. The lifetime is the maximum time allowed for the HMAC Key to exist since its last use. HMAC Key expiration, on the other hand, is a set date in which the HMAC Key will cease to function.
291+
231292
### Login Attempt Logging
232293

233294
By default, only failed login attempts are recorded in the `auth_token_logins` table.

docs/references/authentication/tokens.md

+56-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ Configure **app/Config/AuthToken.php** for your needs.
125125

126126
### Access Token Lifetime
127127

128-
Tokens will expire after a specified amount of time has passed since they have been used.
128+
Tokens will expire after a specified amount of time has passed since they have last been used.
129129

130130
By default, this is set to 1 year.
131131
You can change this value by setting the `$unusedTokenLifetime` value. This is
@@ -137,6 +137,61 @@ that CodeIgniter provides.
137137
public $unusedTokenLifetime = YEAR;
138138
```
139139

140+
141+
## Expiring Access Tokens
142+
143+
By default, the Access Tokens don't expire unless they reach the Access Token's lifetime expiration after their last use date.
144+
145+
Access Tokens can be set to expire through the `generateAccessToken()` method. This takes the expiration date as the `$expiresAt` argument. To update an existing HMAC key expiration date, use `updateAcessTokenExpiration($accessTokenID, $expiresAt)`. To remove it, use `removeAccessTokenExpiration($accessTokenID)`.
146+
147+
`$expiresAt` [Time](https://codeigniter.com/user_guide/libraries/time.html) object
148+
149+
```php
150+
// Expiration date = 2024-11-03 12:00:00
151+
$expiresAt = Time::parse('2024-11-03 12:00:00');
152+
$token = $this->user->generateAccessToken('foo', ['foo.bar'], $expiresAt);
153+
154+
// Expiration date = 2024-11-15 00:00:00
155+
$expiresAt = Time::parse('2024-11-15 00:00:00');
156+
$user->updateAcessTokenExpiration($token->id, $expiresAt);
157+
158+
// Or Expiration date = 1 month + 15 days into the future
159+
$expiresAt = Time::now()->addMonths(1)->addDays(15);
160+
$user->updateAcessTokenExpiration($token->id, $expiresAt);
161+
162+
// Remove the expiration date
163+
$user->removeAccessTokenExpiration($token->id);
164+
```
165+
166+
The following support methods are also available:
167+
168+
`isAccessTokenExpired(AccessToken $accessToken)` - Checks if Access Token is expired. Returns `true` if the Access Token is expired; otherwise, returns `false`.
169+
170+
```php
171+
$expiresAt = Time::parse('2024-11-03 12:00:00');
172+
173+
$token = $this->user->generateAccessToken('foo', ['foo.bar'], $expiresAt);
174+
175+
$this->user->isAccessTokenExpired($token); // Returns true
176+
```
177+
178+
`canAccessTokenExpire(AccessToken $accessToken)` - Returns `true` if the Access Token has a set expiration date; otherwise, returns `false`.
179+
180+
```php
181+
$expiresAt = Time::parse('2024-11-03 12:00:00');
182+
183+
$token = $this->user->generateAccessToken('foo', ['foo.bar'], $expiresAt);
184+
$this->user->canAccessTokenExpire($token2); // Returns false
185+
186+
$token2 = $this->user->generateAccessToken('bar');
187+
$this->user->canAccessTokenExpire($token); // Returns true
188+
```
189+
190+
191+
### Access Token Expiration vs Lifetime
192+
193+
Expiration and lifetime are two different concepts. The lifetime is the maximum time allowed for the token to exist since its last use. Token expiration, on the other hand, is a set date in which the Access Token will cease to function.
194+
140195
### Login Attempt Logging
141196

142197
By default, only failed login attempts are recorded in the `auth_token_logins` table.

phpstan-baseline.php

+8-2
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,13 @@
168168
$ignoreErrors[] = [
169169
'message' => '#^Cannot access property \\$id on array\\<string, string\\>\\|object\\.$#',
170170
'identifier' => 'property.nonObject',
171-
'count' => 7,
171+
'count' => 9,
172+
'path' => __DIR__ . '/src/Commands/Hmac.php',
173+
];
174+
$ignoreErrors[] = [
175+
'message' => '#^Cannot access property \\$expires on array\\<string, string\\>\\|object\\.$#',
176+
'identifier' => 'property.nonObject',
177+
'count' => 3,
172178
'path' => __DIR__ . '/src/Commands/Hmac.php',
173179
];
174180
$ignoreErrors[] = [
@@ -259,7 +265,7 @@
259265
$ignoreErrors[] = [
260266
'message' => '#^Call to function model with CodeIgniter\\\\Shield\\\\Models\\\\UserIdentityModel\\:\\:class is discouraged\\.$#',
261267
'identifier' => 'codeigniter.factoriesClassConstFetch',
262-
'count' => 19,
268+
'count' => 23,
263269
'path' => __DIR__ . '/src/Entities/User.php',
264270
];
265271
$ignoreErrors[] = [

psalm.xml

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
errorBaseline="psalm-baseline.xml"
1212
findUnusedBaselineEntry="false"
1313
findUnusedCode="false"
14+
ensureOverrideAttribute="false"
1415
>
1516
<projectFiles>
1617
<directory name="src/" />

src/Authentication/Authenticators/AccessTokens.php

+13
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,19 @@ public function check(array $credentials): Result
154154

155155
assert($token->last_used_at instanceof Time || $token->last_used_at === null);
156156

157+
// Is expired ?
158+
if (
159+
$token->expires instanceof Time
160+
&& $token->expires->isBefore(
161+
Time::now(),
162+
)
163+
) {
164+
return new Result([
165+
'success' => false,
166+
'reason' => lang('Auth.oldToken'),
167+
]);
168+
}
169+
157170
// Hasn't been used in a long time
158171
if (
159172
$token->last_used_at

src/Authentication/Traits/HasAccessTokens.php

+69-4
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@
1313

1414
namespace CodeIgniter\Shield\Authentication\Traits;
1515

16+
use CodeIgniter\I18n\Time;
17+
use CodeIgniter\Shield\Authentication\Authenticators\AccessTokens;
1618
use CodeIgniter\Shield\Entities\AccessToken;
1719
use CodeIgniter\Shield\Models\UserIdentityModel;
20+
use InvalidArgumentException;
1821

1922
/**
2023
* Trait HasAccessTokens
@@ -34,15 +37,18 @@ trait HasAccessTokens
3437
/**
3538
* Generates a new personal access token for this user.
3639
*
37-
* @param string $name Token name
38-
* @param list<string> $scopes Permissions the token grants
40+
* @param string $name Token name
41+
* @param list<string> $scopes Permissions the token grants
42+
* @param Time|null $expiresAt Expiration date
43+
*
44+
* @throws InvalidArgumentException
3945
*/
40-
public function generateAccessToken(string $name, array $scopes = ['*']): AccessToken
46+
public function generateAccessToken(string $name, array $scopes = ['*'], ?Time $expiresAt = null): AccessToken
4147
{
4248
/** @var UserIdentityModel $identityModel */
4349
$identityModel = model(UserIdentityModel::class);
4450

45-
return $identityModel->generateAccessToken($this, $name, $scopes);
51+
return $identityModel->generateAccessToken($this, $name, $scopes, $expiresAt);
4652
}
4753

4854
/**
@@ -165,4 +171,63 @@ public function setAccessToken(?AccessToken $accessToken): self
165171

166172
return $this;
167173
}
174+
175+
/**
176+
* Checks if the provided Access Token is expired.
177+
*/
178+
public function isAccessTokenExpired(AccessToken $accessToken): bool
179+
{
180+
return $accessToken->expires instanceof Time && $accessToken->expires->isBefore(Time::now());
181+
}
182+
183+
/**
184+
* Sets an expiration for Access Tokens by ID.
185+
*
186+
* @param int $id AccessTokens ID
187+
* @param Time $expiresAt Expiration date
188+
*
189+
* @return bool Returns true if expiration date is set or updated.
190+
*/
191+
public function updateAccessTokenExpiration(int $id, Time $expiresAt): bool
192+
{
193+
/** @var UserIdentityModel $identityModel */
194+
$identityModel = model(UserIdentityModel::class);
195+
$result = $identityModel->setIdentityExpirationById($id, $this, $expiresAt);
196+
197+
if ($result) {
198+
// refresh currentAccessToken with updated data
199+
$this->currentAccessToken = $identityModel->getAccessTokenById($id, $this);
200+
}
201+
202+
return $result;
203+
}
204+
205+
/**
206+
* Removes the expiration date for Access Tokens by ID.
207+
*
208+
* @param int $id AccessTokens ID
209+
*
210+
* @return bool Returns true if expiration date is set or updated.
211+
*/
212+
public function removeAccessTokenExpiration(int $id): bool
213+
{
214+
/** @var UserIdentityModel $identityModel */
215+
$identityModel = model(UserIdentityModel::class);
216+
$result = $identityModel->setIdentityExpirationById($id, $this);
217+
218+
if ($result) {
219+
// refresh currentAccessToken with updated data
220+
$this->currentAccessToken = $identityModel->getAccessTokenById($id, $this);
221+
}
222+
223+
return $result;
224+
}
225+
226+
/**
227+
* Checks if the access token has a set expiration date
228+
*/
229+
public function canAccessTokenExpire(AccessToken $accessToken): bool
230+
{
231+
return $accessToken->expires !== null;
232+
}
168233
}

0 commit comments

Comments
 (0)