Skip to content

Commit 732bd28

Browse files
committed
feat: add expiration date to access token & hmac keys/tokens.
1 parent 58d6d6f commit 732bd28

File tree

10 files changed

+526
-24
lines changed

10 files changed

+526
-24
lines changed

docs/references/authentication/hmac.md

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,58 @@ 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 meet the HMAC Keys lifetime expiration after their last used date.
103+
104+
HMAC keys can be set to expire through the `generateHmacToken()` method. This takes the expiration date as the $expiresAt argument. It's also possible to update an existing HMAC key using `setHmacTokenExpirationById($HmacTokenID, $expiresAt)`
105+
106+
`$expiresAt` Accepts DateTime string formatted as 'Y-m-d h:i:s' or [DateTime relative formats](https://www.php.net/manual/en/datetime.formats.php#datetime.formats.relative) unit symbols (1 day, 2 weeks, 6 months, 1 year) to be added to DateTime 'now'
107+
108+
```php
109+
// Expiration date = 2024-11-03 12:00:00
110+
$token = $this->user->generateHmacToken('foo', ['foo:bar'], '2024-11-03 12:00:00');
111+
112+
// Expiration date = 2024-11-15 00:00:00
113+
$token = $user->setHmacTokenExpirationById($token->id, '2024-11-15 00:00:00');
114+
115+
// Or Expiration date = now() + 1 month + 15 days
116+
$token = $user->setHmacTokenExpirationById($token->id, '1 month 15 days');
117+
```
118+
119+
The following support methods are also available:
120+
121+
`hasHmacTokenExpired(AccessToken $HmacToken)` - Checks if the given HMAC key has expired. Returns `true` if the HMAC key has expired, `false` if not, and `null` if the expire date is null.
122+
123+
```php
124+
$token = $this->user->generateAccessToken('foo', ['foo:bar'], '2024-11-03 12:00:00');
125+
126+
$this->user->hasHmacTokenExpired($token); // Returns true
127+
```
128+
129+
`getHmacTokenTimeToExpire(AccessToken $accessToken, string $format = "date" | "human")` - Checks if the given HMAC key has expired. Returns `true` if HMAC key has expired, `false` if not, and `null` if the expire date is not set.
130+
131+
```php
132+
$token = $this->user->generateHmacToken('foo', ['foo:bar']);
133+
134+
$this->user->getHmacTokenTimeToExpire($token, 'date'); // Returns null
135+
136+
// Assuming current time is: 2024-11-04 20:00:00
137+
$token = $this->user->generateHmacToken('foo', ['foo:bar'], '2024-11-03 12:00:00');
138+
139+
$this->user->getHmacTokenTimeToExpire($token, 'date'); // 2024-11-03 12:00:00
140+
$this->user->getHmacTokenTimeToExpire($token, 'human'); // 1 day ago
141+
142+
$token = $this->user->generateHmacToken('foo', ['foo:bar'], '2026-01-06 12:00:00');
143+
$this->user->getHmacTokenTimeToExpire($token, 'human'); // in 1 year
144+
```
145+
146+
You can also easily set all existing HMAC keys/tokens as expired with the `spark` command:
147+
```
148+
php spark shield:hmac expireAll
149+
```
150+
**Careful!** This command 'expires' _all_ keys for _all_ users.
151+
100152
## Retrieving HMAC Keys
101153

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

218270
### HMAC Keys Lifetime
219271

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

222274
By default, this is set to 1 year. You can change this value by setting the `$unusedTokenLifetime`
223275
value. This is in seconds so that you can use the
@@ -228,6 +280,9 @@ that CodeIgniter provides.
228280
public $unusedTokenLifetime = YEAR;
229281
```
230282

283+
### HMAC Keys Expiration vs Lifetime
284+
Expiration and Lifetime are 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.
285+
231286
### Login Attempt Logging
232287

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

docs/references/authentication/tokens.md

Lines changed: 51 additions & 1 deletion
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 last have 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,56 @@ 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 meet the Access Token lifetime expiration after their last used date.
144+
145+
Access Tokens can be set to expire through the `generateAccessToken()` method. This takes the expiration date as the $expiresAt argument. It's also possible to update an existing HMAC key using `setAccessTokenById($HmacTokenID, $expiresAt)`
146+
147+
`$expiresAt` Accepts DateTime string formatted as 'Y-m-d h:i:s' or [DateTime relative formats](https://www.php.net/manual/en/datetime.formats.php#datetime.formats.relative) unit symbols (1 day, 2 weeks, 6 months, 1 year) to be added to DateTime 'now'
148+
149+
```php
150+
// Expiration date = 2024-11-03 12:00:00
151+
$token = $this->user->generateAccessToken('foo', ['foo:bar'], '2024-11-03 12:00:00');
152+
153+
// Expiration date = 2024-11-15 00:00:00
154+
$user->setAccessTokenExpirationById($token->id, '2024-11-15 00:00:00');
155+
156+
// Or Expiration date = now() + 1 month + 15 days
157+
$user->setAccessTokenExpirationById($token->id, '1 month 15 days');
158+
```
159+
160+
The following support methods are also available:
161+
162+
`hasAccessTokenExpired(AccessToken $accessToken)` - Checks if the given Access Token has expired. Returns `true` if the Access Token has expired, `false` if not, and `null` if the expire date is not set.
163+
164+
```php
165+
$token = $this->user->generateAccessToken('foo', ['foo:bar'], '2024-11-03 12:00:00');
166+
167+
$this->user->hasAccessTokenExpired($token); // Returns true
168+
```
169+
170+
`getAccessTokenTimeToExpire(AccessToken $accessToken, string $format = "date" | "human")` - Checks if the given Access Token has expired. Returns `true` if Access Token has expired, `false` if not, and `null` if the expire date is null.
171+
172+
```php
173+
$token = $this->user->generateAccessToken('foo', ['foo:bar']);
174+
175+
$this->user->getAccessTokenTimeToExpire($token, 'date'); // Returns null
176+
177+
// Assuming current time is: 2024-11-04 20:00:00
178+
$token = $this->user->generateAccessToken('foo', ['foo:bar'], '2024-11-03 12:00:00');
179+
180+
$this->user->getAccessTokenTimeToExpire($token, 'date'); // 2024-11-03 12:00:00
181+
$this->user->getAccessTokenTimeToExpire($token, 'human'); // 1 day ago
182+
183+
$token = $this->user->generateAccessToken('foo', ['foo:bar'], '2026-01-06 12:00:00');
184+
$this->user->getAccessTokenTimeToExpire($token, 'human'); // in 1 year
185+
```
186+
187+
### Access Token Expiration vs Lifetime
188+
Expiration and Lifetime are 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.
189+
140190
### Login Attempt Logging
141191

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

src/Authentication/Authenticators/AccessTokens.php

Lines changed: 13 additions & 0 deletions
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
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

Lines changed: 72 additions & 4 deletions
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 string $expiresAt Sets token expiration date. Accepts DateTime string formatted as 'Y-m-d h:i:s' or DateTime relative formats (1 day, 2 weeks, 6 months, 1 year) to be added to DateTime 'now'
43+
*
44+
* @throws InvalidArgumentException
3945
*/
40-
public function generateAccessToken(string $name, array $scopes = ['*']): AccessToken
46+
public function generateAccessToken(string $name, array $scopes = ['*'], ?string $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,66 @@ public function setAccessToken(?AccessToken $accessToken): self
165171

166172
return $this;
167173
}
174+
175+
/**
176+
* Checks if the provided Access Token has expired.
177+
*
178+
* @return false|true|null Returns true if Access Token has expired, false if not, and null if the expire field is null
179+
*/
180+
public function hasAccessTokenExpired(?AccessToken $accessToken): bool|null
181+
{
182+
if (null === $accessToken->expires) {
183+
return null;
184+
}
185+
186+
return $accessToken->expires->isBefore(Time::now());
187+
}
188+
189+
/**
190+
* Returns formatted date to expiration for provided AccessToken
191+
*
192+
* @param AcessToken $accessToken AccessToken
193+
* @param string $format The return format - "date" or "human". Date is 'Y-m-d h:i:s', human is 'in 2 weeks'
194+
*
195+
* @return false|true|null Returns true if Access Token has expired, false if not and null if the expire field is null
196+
*
197+
* @throws InvalidArgumentException
198+
*/
199+
public function getAccessTokenTimeToExpire(?AccessToken $accessToken, string $format = 'date'): string|null
200+
{
201+
if (null === $accessToken->expires) {
202+
return null;
203+
}
204+
205+
switch ($format) {
206+
case 'date':
207+
return $accessToken->expires->toLocalizedString();
208+
209+
case 'human':
210+
return $accessToken->expires->humanize();
211+
212+
default:
213+
throw new InvalidArgumentException('getAccessTokenTimeToExpire(): $format argument is invalid. Expects string with "date" or "human".');
214+
}
215+
}
216+
217+
/**
218+
* Sets an expiration for Access Tokens by ID.
219+
*
220+
* @param int $id AccessTokens ID
221+
* @param string $expiresAt Expiration date. Accepts DateTime string formatted as 'Y-m-d h:i:s' or DateTime relative formats (1 day, 2 weeks, 6 months, 1 year) to be added to DateTime 'now'
222+
*/
223+
public function setAccessTokenExpirationById(int $id, string $expiresAt): bool
224+
{
225+
/** @var UserIdentityModel $identityModel */
226+
$identityModel = model(UserIdentityModel::class);
227+
$result = $identityModel->setIdentityExpirationById($id, $this, $expiresAt, AccessTokens::ID_TYPE_ACCESS_TOKEN);
228+
229+
if ($result) {
230+
// refresh currentAccessToken with updated data
231+
$this->currentAccessToken = $identityModel->getAccessTokenById($id, $this);
232+
}
233+
234+
return $result;
235+
}
168236
}

src/Authentication/Traits/HasHmacTokens.php

Lines changed: 73 additions & 4 deletions
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\HmacSha256;
1618
use CodeIgniter\Shield\Entities\AccessToken;
1719
use CodeIgniter\Shield\Models\UserIdentityModel;
20+
use InvalidArgumentException;
1821
use ReflectionException;
1922

2023
/**
@@ -35,17 +38,19 @@ trait HasHmacTokens
3538
/**
3639
* Generates a new personal HMAC token for this user.
3740
*
38-
* @param string $name Token name
39-
* @param list<string> $scopes Permissions the token grants
41+
* @param string $name Token name
42+
* @param list<string> $scopes Permissions the token grants
43+
* @param string $expiresAt Sets token expiration date. Accepts DateTime string formatted as 'Y-m-d h:i:s' or DateTime relative formats (1 day, 2 weeks, 6 months, 1 year) to be added to DateTime 'now'
4044
*
45+
* @throws InvalidArgumentException
4146
* @throws ReflectionException
4247
*/
43-
public function generateHmacToken(string $name, array $scopes = ['*']): AccessToken
48+
public function generateHmacToken(string $name, array $scopes = ['*'], ?string $expiresAt = null): AccessToken
4449
{
4550
/** @var UserIdentityModel $identityModel */
4651
$identityModel = model(UserIdentityModel::class);
4752

48-
return $identityModel->generateHmacToken($this, $name, $scopes);
53+
return $identityModel->generateHmacToken($this, $name, $scopes, $expiresAt);
4954
}
5055

5156
/**
@@ -156,4 +161,68 @@ public function setHmacToken(?AccessToken $accessToken): self
156161

157162
return $this;
158163
}
164+
165+
/**
166+
* Checks if the provided Access Token has expired.
167+
*
168+
* @return false|true|null Returns true if Access Token has expired, false if not, and null if the expire field is null
169+
*/
170+
public function hasHmacTokenExpired(?AccessToken $accessToken): bool|null
171+
{
172+
if (null === $accessToken->expires) {
173+
return null;
174+
}
175+
176+
return $accessToken->expires->isBefore(Time::now());
177+
}
178+
179+
/**
180+
* Returns formatted date to expiration for provided Hmac Key/Token.
181+
*
182+
* @param AcessToken $accessToken AccessToken
183+
* @param string $format The return format - "date" or "human". Date is 'Y-m-d h:i:s', human is 'in 2 weeks'
184+
*
185+
* @return false|true|null Returns true if Access Token has expired, false if not and null if the expire field is null
186+
*
187+
* @throws InvalidArgumentException
188+
*/
189+
public function getHmacTokenTimeToExpire(?AccessToken $accessToken, string $format = 'date'): string|null
190+
{
191+
if (null === $accessToken->expires) {
192+
return null;
193+
}
194+
195+
switch ($format) {
196+
case 'date':
197+
return $accessToken->expires->toLocalizedString();
198+
199+
case 'human':
200+
return $accessToken->expires->humanize();
201+
202+
default:
203+
throw new InvalidArgumentException('getHmacTokenTimeToExpire(): $format argument is invalid. Expects string with "date" or "human".');
204+
}
205+
}
206+
207+
/**
208+
* Sets an expiration for Hmac Key/Token by ID.
209+
*
210+
* @param int $id AccessTokens ID
211+
* @param string $expiresAt Expiration date. Accepts DateTime string formatted as 'Y-m-d h:i:s' or DateTime relative formats (1 day, 2 weeks, 6 months, 1 year) to be added to DateTime 'now'
212+
*
213+
* @return false|true|null Returns true if token is updated, false if not.
214+
*/
215+
public function setHmacTokenExpirationById(int $id, string $expiresAt): bool
216+
{
217+
/** @var UserIdentityModel $identityModel */
218+
$identityModel = model(UserIdentityModel::class);
219+
$result = $identityModel->setIdentityExpirationById($id, $this, $expiresAt, HmacSha256::ID_TYPE_HMAC_TOKEN);
220+
221+
if ($result) {
222+
// refresh currentAccessToken with updated data
223+
$this->currentAccessToken = $identityModel->getHmacTokenById($id, $this);
224+
}
225+
226+
return $result;
227+
}
159228
}

0 commit comments

Comments
 (0)