Skip to content

Commit 3d372e6

Browse files
authored
[2.x] Add sanctum:prune-expired command for removing expired tokens. (#348)
* [2.x] Add sanctum:prune-expired command for removing expired tokens. * Removed the assertDatabaseCount() method from PruneExpiredTest code * Fixed style in the PruneExpired command
1 parent bc53d22 commit 3d372e6

File tree

4 files changed

+157
-0
lines changed

4 files changed

+157
-0
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"require": {
1717
"php": "^7.2|^8.0",
1818
"ext-json": "*",
19+
"illuminate/console": "^6.9|^7.0|^8.0|^9.0",
1920
"illuminate/contracts": "^6.9|^7.0|^8.0|^9.0",
2021
"illuminate/database": "^6.9|^7.0|^8.0|^9.0",
2122
"illuminate/support": "^6.9|^7.0|^8.0|^9.0"

src/Console/Commands/PruneExpired.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace Laravel\Sanctum\Console\Commands;
4+
5+
use Illuminate\Console\Command;
6+
use Laravel\Sanctum\Sanctum;
7+
8+
class PruneExpired extends Command
9+
{
10+
/**
11+
* The name and signature of the console command.
12+
*
13+
* @var string
14+
*/
15+
protected $signature = 'sanctum:prune-expired {--hours=24 : The number of hours to retain expired Sanctum tokens}';
16+
17+
/**
18+
* The console command description.
19+
*
20+
* @var string
21+
*/
22+
protected $description = 'Prune tokens expired for more than specified number of hours.';
23+
24+
/**
25+
* Execute the console command.
26+
*
27+
* @return int
28+
*/
29+
public function handle()
30+
{
31+
if ($expiration = config('sanctum.expiration')) {
32+
$model = Sanctum::$personalAccessTokenModel;
33+
34+
$hours = $this->option('hours');
35+
36+
$model::where('created_at', '<', now()->subMinutes($expiration + ($hours * 60)))->delete();
37+
38+
$this->info("Tokens expired for more than {$hours} hours pruned successfully.");
39+
}
40+
41+
$this->warn('Expiration value not specified in configuration file.');
42+
}
43+
}

src/SanctumServiceProvider.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Illuminate\Support\Facades\Auth;
88
use Illuminate\Support\Facades\Route;
99
use Illuminate\Support\ServiceProvider;
10+
use Laravel\Sanctum\Console\Commands\PruneExpired;
1011
use Laravel\Sanctum\Http\Controllers\CsrfCookieController;
1112
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;
1213

@@ -48,6 +49,10 @@ public function boot()
4849
$this->publishes([
4950
__DIR__.'/../config/sanctum.php' => config_path('sanctum.php'),
5051
], 'sanctum-config');
52+
53+
$this->commands([
54+
PruneExpired::class,
55+
]);
5156
}
5257

5358
$this->defineRoutes();

tests/PruneExpiredTest.php

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
namespace Laravel\Sanctum\Tests;
4+
5+
use Illuminate\Foundation\Auth\User;
6+
use Laravel\Sanctum\Contracts\HasApiTokens as HasApiTokensContract;
7+
use Laravel\Sanctum\HasApiTokens;
8+
use Laravel\Sanctum\PersonalAccessToken;
9+
use Laravel\Sanctum\SanctumServiceProvider;
10+
use Orchestra\Testbench\TestCase;
11+
12+
class PruneExpiredTest extends TestCase
13+
{
14+
protected function getEnvironmentSetUp($app)
15+
{
16+
$app['config']->set('database.default', 'testbench');
17+
18+
$app['config']->set('database.connections.testbench', [
19+
'driver' => 'sqlite',
20+
'database' => ':memory:',
21+
'prefix' => '',
22+
]);
23+
}
24+
25+
public function test_can_delete_expired_tokens_with_integer_expiration()
26+
{
27+
$this->loadLaravelMigrations(['--database' => 'testbench']);
28+
$this->artisan('migrate', ['--database' => 'testbench'])->run();
29+
30+
config(['sanctum.expiration' => 60]);
31+
32+
$user = UserForPruneExpiredTest::forceCreate([
33+
'name' => 'Taylor Otwell',
34+
'email' => 'taylor@laravel.com',
35+
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi',
36+
]);
37+
38+
$token_1 = PersonalAccessToken::forceCreate([
39+
'tokenable_id' => $user->id,
40+
'tokenable_type' => get_class($user),
41+
'name' => 'Test_1',
42+
'token' => hash('sha256', 'test_1'),
43+
'created_at' => now()->subMinutes(181),
44+
]);
45+
46+
$token_2 = PersonalAccessToken::forceCreate([
47+
'tokenable_id' => $user->id,
48+
'tokenable_type' => get_class($user),
49+
'name' => 'Test_2',
50+
'token' => hash('sha256', 'test_2'),
51+
'created_at' => now()->subMinutes(179),
52+
]);
53+
54+
$token_3 = PersonalAccessToken::forceCreate([
55+
'tokenable_id' => $user->id,
56+
'tokenable_type' => get_class($user),
57+
'name' => 'Test_3',
58+
'token' => hash('sha256', 'test_3'),
59+
'created_at' => now()->subMinutes(121),
60+
]);
61+
62+
$this->artisan('sanctum:prune-expired --hours=2')
63+
->expectsOutput('Tokens expired for more than 2 hours pruned successfully.');
64+
65+
$this->assertDatabaseMissing('personal_access_tokens', ['name' => 'Test_1']);
66+
$this->assertDatabaseHas('personal_access_tokens', ['name' => 'Test_2']);
67+
$this->assertDatabaseHas('personal_access_tokens', ['name' => 'Test_3']);
68+
}
69+
70+
public function test_cant_delete_expired_tokens_with_null_expiration()
71+
{
72+
$this->loadLaravelMigrations(['--database' => 'testbench']);
73+
$this->artisan('migrate', ['--database' => 'testbench'])->run();
74+
75+
config(['sanctum.expiration' => null]);
76+
77+
$user = UserForPruneExpiredTest::forceCreate([
78+
'name' => 'Taylor Otwell',
79+
'email' => 'taylor@laravel.com',
80+
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi',
81+
]);
82+
83+
$token = PersonalAccessToken::forceCreate([
84+
'tokenable_id' => $user->id,
85+
'tokenable_type' => get_class($user),
86+
'name' => 'Test',
87+
'token' => hash('sha256', 'test'),
88+
'created_at' => now()->subMinutes(70),
89+
]);
90+
91+
$this->artisan('sanctum:prune-expired --hours=2')
92+
->expectsOutput('Expiration value not specified in configuration file.');
93+
94+
$this->assertDatabaseHas('personal_access_tokens', ['name' => 'Test']);
95+
}
96+
97+
protected function getPackageProviders($app)
98+
{
99+
return [SanctumServiceProvider::class];
100+
}
101+
}
102+
103+
class UserForPruneExpiredTest extends User implements HasApiTokensContract
104+
{
105+
use HasApiTokens;
106+
107+
protected $table = 'users';
108+
}

0 commit comments

Comments
 (0)