{"id":51958,"date":"2025-08-30T12:31:18","date_gmt":"2025-08-30T12:31:18","guid":{"rendered":"https:\/\/www.devopsschool.com\/blog\/?p=51958"},"modified":"2025-08-30T12:31:18","modified_gmt":"2025-08-30T12:31:18","slug":"laravel-oauth_access_tokens-clean-up-process","status":"publish","type":"post","link":"https:\/\/www.devopsschool.com\/blog\/laravel-oauth_access_tokens-clean-up-process\/","title":{"rendered":"Laravel: oauth_access_tokens clean up process"},"content":{"rendered":"\n<p>using Laravel Passport\u2014the <code>oauth_access_tokens<\/code> table balloons when tokens aren\u2019t expiring, not being revoked on logout, or not being purged. Here\u2019s a practical, safe cleanup + prevention plan you can apply right now.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">1) Inspect what\u2019s growing<\/h1>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">-- Table sizes (MB)\nSELECT TABLE_NAME, ROUND((DATA_LENGTH+INDEX_LENGTH)\/<span class=\"hljs-number\">1024<\/span>\/<span class=\"hljs-number\">1024<\/span>,<span class=\"hljs-number\">2<\/span>) <span class=\"hljs-keyword\">AS<\/span> MB\nFROM information_schema.TABLES\nWHERE TABLE_SCHEMA = DATABASE()\n  <span class=\"hljs-keyword\">AND<\/span> TABLE_NAME IN (<span class=\"hljs-string\">'oauth_access_tokens'<\/span>,<span class=\"hljs-string\">'oauth_refresh_tokens'<\/span>)\nORDER BY MB DESC;\n\n-- How many active (not revoked) tokens per user\nSELECT user_id, COUNT(*) <span class=\"hljs-keyword\">AS<\/span> tokens\nFROM oauth_access_tokens\nWHERE revoked = <span class=\"hljs-number\">0<\/span>\nGROUP BY user_id\nORDER BY tokens DESC\nLIMIT <span class=\"hljs-number\">20<\/span>;\n\n-- Expired tokens still present?\nSELECT COUNT(*) \nFROM oauth_access_tokens\nWHERE expires_at IS NOT <span class=\"hljs-keyword\">NULL<\/span> <span class=\"hljs-keyword\">AND<\/span> expires_at &lt; NOW();\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h1 class=\"wp-block-heading\">2) One-time purge (safe cleanup)<\/h1>\n\n\n\n<p><strong>Backup first.<\/strong> Then, if you\u2019re on Passport 10+:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">php<\/span> <span class=\"hljs-selector-tag\">artisan<\/span> <span class=\"hljs-selector-tag\">passport<\/span><span class=\"hljs-selector-pseudo\">:purge<\/span> <span class=\"hljs-selector-tag\">--revoked<\/span> <span class=\"hljs-selector-tag\">--expired<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This removes revoked and expired access\/refresh tokens.<\/p>\n\n\n\n<p>If you don\u2019t have the purge command, do it manually:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">-- Revoke + <span class=\"hljs-keyword\">delete<\/span> obviously old tokens (example: older than <span class=\"hljs-number\">90<\/span> days)\nUPDATE oauth_access_tokens SET revoked = <span class=\"hljs-number\">1<\/span>\nWHERE (expires_at IS NOT NULL AND expires_at &lt; NOW()) OR created_at &lt; NOW() - INTERVAL <span class=\"hljs-number\">90<\/span> DAY;\n\n-- Clean up refresh tokens linked to revoked access tokens\nDELETE rt FROM oauth_refresh_tokens rt\nJOIN oauth_access_tokens at ON rt.access_token_id = at.id\nWHERE at.revoked = <span class=\"hljs-number\">1<\/span>;\n\n-- Optionally <span class=\"hljs-keyword\">delete<\/span> the revoked access tokens themselves\nDELETE FROM oauth_access_tokens WHERE revoked = <span class=\"hljs-number\">1<\/span>;\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Then reclaim disk space (if you use file-per-table InnoDB):<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">OPTIMIZE TABLE oauth_access_tokens;\nOPTIMIZE TABLE oauth_refresh_tokens;\n<\/code><\/span><\/pre>\n\n\n<h1 class=\"wp-block-heading\">3) Fix the root causes<\/h1>\n\n\n\n<h3 class=\"wp-block-heading\">A) Set short, sane expirations<\/h3>\n\n\n\n<p>In <code>App\\Providers\\AuthServiceProvider.php<\/code>:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Laravel<\/span>\\<span class=\"hljs-title\">Passport<\/span>\\<span class=\"hljs-title\">Passport<\/span>;\n\n<span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">boot<\/span><span class=\"hljs-params\">()<\/span>\n<\/span>{\n    <span class=\"hljs-keyword\">$this<\/span>-&gt;registerPolicies();\n\n    Passport::tokensExpireIn(now()-&gt;addHours(<span class=\"hljs-number\">2<\/span>));             <span class=\"hljs-comment\">\/\/ access tokens<\/span>\n    Passport::refreshTokensExpireIn(now()-&gt;addDays(<span class=\"hljs-number\">14<\/span>));      <span class=\"hljs-comment\">\/\/ refresh tokens<\/span>\n    Passport::personalAccessTokensExpireIn(now()-&gt;addMonths(<span class=\"hljs-number\">3<\/span>));\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Deploy, then <code>php artisan config:clear &amp;&amp; php artisan cache:clear<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">B) Revoke on logout (don\u2019t leave tokens hanging)<\/h3>\n\n\n\n<p>If you\u2019re issuing access tokens on login, make sure logout revokes them:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">logout<\/span><span class=\"hljs-params\">(Request $request)<\/span>\n<\/span>{\n    $token = $request-&gt;user()-&gt;token();\n    <span class=\"hljs-keyword\">if<\/span> ($token) {\n        $token-&gt;revoke();\n        <span class=\"hljs-comment\">\/\/ Also revoke its refresh tokens<\/span>\n        \\DB::table(<span class=\"hljs-string\">'oauth_refresh_tokens'<\/span>)\n          -&gt;where(<span class=\"hljs-string\">'access_token_id'<\/span>, $token-&gt;id)\n          -&gt;update(&#91;<span class=\"hljs-string\">'revoked'<\/span> =&gt; <span class=\"hljs-keyword\">true<\/span>]);\n    }\n    <span class=\"hljs-keyword\">return<\/span> response()-&gt;json(&#91;<span class=\"hljs-string\">'message'<\/span> =&gt; <span class=\"hljs-string\">'Logged out'<\/span>]);\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>If you want to nuke <strong>all<\/strong> a user\u2019s tokens on logout:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">$request-&gt;user()-&gt;tokens()-&gt;delete();\n\\DB::table(<span class=\"hljs-string\">'oauth_refresh_tokens'<\/span>)-&gt;whereIn(\n  <span class=\"hljs-string\">'access_token_id'<\/span>,\n  \\DB::table(<span class=\"hljs-string\">'oauth_access_tokens'<\/span>)-&gt;where(<span class=\"hljs-string\">'user_id'<\/span>, $request-&gt;user()-&gt;id)-&gt;pluck(<span class=\"hljs-string\">'id'<\/span>)\n)-&gt;delete();\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\">C) Don\u2019t issue a new token on every request<\/h3>\n\n\n\n<p>Audit your login\/auth flow\u2014ensure you <strong>only<\/strong> create a token at login (or first app start) and then reuse it. If you\u2019re creating tokens during each API call, that\u2019s the main culprit.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">D) Schedule automatic purging<\/h3>\n\n\n\n<p>In <code>app\/Console\/Kernel.php<\/code>:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-keyword\">protected<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">schedule<\/span><span class=\"hljs-params\">(Schedule $schedule)<\/span>\n<\/span>{\n    <span class=\"hljs-comment\">\/\/ Passport 10+:<\/span>\n    $schedule-&gt;command(<span class=\"hljs-string\">'passport:purge --revoked --expired'<\/span>)-&gt;dailyAt(<span class=\"hljs-string\">'02:30'<\/span>);\n\n    <span class=\"hljs-comment\">\/\/ Optional: extra safety\u2014purge very old tokens regardless<\/span>\n    <span class=\"hljs-comment\">\/\/ $schedule-&gt;call(fn () =&gt; \\DB::table('oauth_access_tokens')<\/span>\n    <span class=\"hljs-comment\">\/\/    -&gt;where('created_at','&lt;',now()-&gt;subMonths(6))-&gt;delete()<\/span>\n    <span class=\"hljs-comment\">\/\/ )-&gt;weeklyOn(1, '03:00');<\/span>\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Ensure your server cron runs Laravel\u2019s scheduler:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">* * * * * cd \/path\/to\/app &amp;&amp; php artisan schedule:run &gt;&gt; <span class=\"hljs-regexp\">\/dev\/<\/span><span class=\"hljs-literal\">null<\/span> <span class=\"hljs-number\">2<\/span>&gt;&amp;<span class=\"hljs-number\">1<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h1 class=\"wp-block-heading\">4) If you\u2019re not tied to Passport<\/h1>\n\n\n\n<p>For SPAs and first-party mobile apps, consider <strong>Laravel Sanctum<\/strong>. It\u2019s lighter (<code>personal_access_tokens<\/code>), easier to manage, and usually grows less aggressively.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">5) Quick \u201cemergency\u201d reduction<\/h1>\n\n\n\n<p>If you must free space fast and accept losing old sessions:<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">-- Delete tokens older than 30 days (adjust to your risk appetite)\nDELETE rt FROM oauth_refresh_tokens rt\nJOIN oauth_access_tokens at ON rt.access_token_id = at.id\nWHERE at.created_at &lt; NOW() - INTERVAL 30 DAY;\n\nDELETE FROM oauth_access_tokens\nWHERE created_at &lt; NOW() - INTERVAL 30 DAY;\n<\/code><\/span><\/pre>\n\n\n<p>Then <code>OPTIMIZE TABLE<\/code> as above.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>using Laravel Passport\u2014the oauth_access_tokens table balloons when tokens aren\u2019t expiring, not being revoked on logout, or not being purged. Here\u2019s a practical, safe cleanup + prevention plan you can apply&#8230; <\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"_joinchat":[],"footnotes":""},"categories":[2],"tags":[],"class_list":["post-51958","post","type-post","status-publish","format-standard","hentry","category-uncategorised"],"_links":{"self":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/51958","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/comments?post=51958"}],"version-history":[{"count":1,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/51958\/revisions"}],"predecessor-version":[{"id":51959,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/51958\/revisions\/51959"}],"wp:attachment":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/media?parent=51958"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/categories?post=51958"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/tags?post=51958"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}