1<?php
2
3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4// See the LICENCE file in the repository root for full licence text.
5
6namespace App\Console\Commands;
7
8use DB;
9use Illuminate\Console\Command;
10use Laravel\Passport\Token;
11
12/**
13 * Removes expired tokens and auth codes from the OAuth tables.
14 * It uses chunkById which is much slower than a straight batch delete, but doesn't lock the entire table while deleting.
15 */
16class OAuthDeleteExpiredTokens extends Command
17{
18 protected $signature = 'oauth:delete-expired-tokens';
19
20 protected $description = 'Deletes expired OAuth tokens';
21
22 /** @var \Carbon\Carbon */
23 private $expiredBefore;
24
25 public function handle()
26 {
27 $this->expiredBefore = now()->subDays($GLOBALS['cfg']['osu']['oauth']['retain_expired_tokens_days']);
28 $this->line("Deleting before {$this->expiredBefore}");
29
30 $this->deleteAuthCodes();
31 $this->deleteAccessTokens();
32 $this->deleteClientGrantTokens();
33 }
34
35 /**
36 * Removes refresh tokens and associated access tokens.
37 *
38 * @return void
39 */
40 private function deleteAccessTokens()
41 {
42 $refreshTokensQuery = DB::table('oauth_refresh_tokens')
43 ->where('expires_at', '<', $this->expiredBefore)
44 ->select('id', 'access_token_id', 'expires_at');
45 $refreshTokensTotal = (clone $refreshTokensQuery)->count();
46
47 $progress = $this->output->createProgressBar($refreshTokensTotal);
48 $progress->setFormat('very_verbose');
49
50 $accessTokensDeleted = 0;
51 $refreshTokensDeleted = 0;
52 $refreshTokensQuery->chunkById(1000, function ($chunk) use (&$accessTokensDeleted, &$refreshTokensDeleted, $progress) {
53 // This assumes the refresh token always has a longer valid lifetime than the access token.
54 $accessTokensDeleted += Token::whereIn('id', $chunk->pluck('access_token_id'))->delete();
55 $refreshTokensDeleted += DB::table('oauth_refresh_tokens')->whereIn('id', $chunk->pluck('id'))->delete();
56 $progress->advance($chunk->count());
57 }, 'expires_at');
58
59 $progress->finish();
60 $this->line('');
61 $this->line("Deleted {$accessTokensDeleted} expired access tokens.");
62 $this->line("Deleted {$refreshTokensDeleted} expired refresh tokens.");
63 }
64
65 /**
66 * Removes auth codes.
67 *
68 * @return void
69 */
70 private function deleteAuthCodes()
71 {
72 $query = DB::table('oauth_auth_codes')->where('expires_at', '<', $this->expiredBefore)->select('id', 'expires_at');
73 $total = (clone $query)->count();
74
75 $progress = $this->output->createProgressBar($total);
76 $progress->setFormat('very_verbose');
77
78 $deleted = 0;
79 $query->chunkById(1000, function ($chunk) use (&$deleted, $progress) {
80 $deleted += DB::table('oauth_auth_codes')->whereIn('id', $chunk->pluck('id'))->delete();
81 $progress->advance($chunk->count());
82 }, 'expires_at');
83
84 $progress->finish();
85 $this->line('');
86 $this->line("Deleted {$deleted} expired auth codes.");
87 }
88
89 /**
90 * Removes client credential grant tokens. These are access tokens with no associated user id.
91 *
92 * @return void
93 */
94 private function deleteClientGrantTokens()
95 {
96 $query = Token::where('user_id', null)->where('expires_at', '<', $this->expiredBefore)->select('id', 'expires_at');
97 $total = (clone $query)->count();
98
99 $progress = $this->output->createProgressBar($total);
100 $progress->setFormat('very_verbose');
101
102 $deleted = 0;
103 $query->chunkById(1000, function ($chunk) use (&$deleted, $progress) {
104 $deleted += Token::whereIn('id', $chunk->pluck('id'))->delete();
105 $progress->advance($chunk->count());
106 }, 'expires_at');
107
108 $progress->finish();
109 $this->line('');
110 $this->line("Deleted {$deleted} expired client grant tokens.");
111 }
112}