Laravel Sanctum tokens never expire by default — how do the expiration config key, createToken's expiresAt argument and sanctum:prune-expired work together?
answer
- null means forever
- minutes counted from created_at
- expires_at column, third argument
- both checks must pass
- --hours=24, only when scheduled
basics
~20 sconfig('sanctum.expiration') defaults to null, so tokens never expire. When set, it counts minutes from created_at; createToken's third argument sets expires_at. A token failing either check is rejected. sanctum:prune-expired deletes long-expired rows, but only if you schedule it.
solid answer
~40 sSanctum has two independent expiry rules. The global `expiration` key in `config/sanctum.php` (default `null`) is a number of minutes measured from each token's `created_at`. The per-token rule is the `expires_at` column, set by `createToken($name, $abilities, $expiresAt)`. The guard rejects a token if **either** has passed, so the earlier deadline wins, even though the config comment says the global value overrides `expires_at`. An expired token simply fails authentication (401); its row stays until deleted. `php artisan sanctum:prune-expired --hours=24` deletes rows whose `expires_at` is more than 24 hours past and, when `expiration` is set, rows older than `expiration` minutes plus the grace window; with `expiration` null it only warns about the second pass. Nothing runs it unless you schedule it. SPA session auth is unaffected by either setting.
code
php · 14 lines<?php
use Illuminate\Support\Facades\Schedule;
use Laravel\Sanctum\PersonalAccessToken;
use Laravel\Sanctum\Sanctum;
// routes/console.php
Schedule::command('sanctum:prune-expired --hours=24')->daily();
// AppServiceProvider::boot(): also reject tokens idle for 30 days
Sanctum::authenticateAccessTokensUsing(
fn (PersonalAccessToken $token, bool $isValid) => $isValid
&& ($token->last_used_at === null || $token->last_used_at->gt(now()->subDays(30)))
);go deeper
Remember that expiration is null by default, so tokens last until deleted, and that createToken takes an expiry as its third argument.
Explain the two rules — minutes from created_at and the expires_at column — and that the guard enforces both.
Set a token lifetime policy for mobile clients, schedule pruning, and add idle revocation through the authentication callback.
Balance forced re-login for mobile users against the exposure window of long-lived bearer tokens on devices you cannot control.
## The default: tokens live until deleted Sanctum's published `config/sanctum.php` ships `'expiration' => null`. With that value and no per-token expiry, a personal access token stays valid until its row is deleted. That fits the docs' original use case — personal tokens a user generates on a settings page — but it is a liability for a mobile app whose tokens may sit on lost phones for years. ## Two expiry rules, both enforced | Rule | Where it is set | Measured from | Scope | |---|---|---|---| | Global `expiration` | `config/sanctum.php` (minutes) | `created_at` | every token | | Per-token `expires_at` | third argument of `createToken()` | absolute timestamp | one token | The `sanctum` guard's validity check is, in effect: 1. if `expiration` is set, `created_at` must be later than now minus `expiration` minutes; 2. if `expires_at` is set, it must not be in the past; 3. if the guard is tied to a provider, the token's owner must be that provider's model; 4. an optional callback registered with `Sanctum::authenticateAccessTokensUsing()` receives the token and the result and may override it. Rules 1 and 2 are joined with **and**. The config file's comment says the global value "will override" `expires_at`, but the guard applies both, so the **stricter** one wins. With `expiration = 525600` (a year) and `expires_at` one week out, the token dies after a week. The source is what runs; trust it over the comment. First-party SPA requests authenticated by the session never touch these rules; the session lifetime governs them. ## What expiry does not do - It does **not** delete the row. An expired token fails authentication, and the row stays until something removes it. - It does **not** refresh anything. Sanctum has no refresh tokens; a mobile client whose token expired signs in again and gets a new one. - It does **not** measure inactivity. `last_used_at` is written on each authenticated request, but nothing enforces an idle timeout unless your callback does. ## `sanctum:prune-expired` The command's signature is `sanctum:prune-expired {--hours=24}`. It runs two passes: - delete rows whose `expires_at` is earlier than now minus `--hours`; - if `expiration` is configured, delete rows whose `created_at` is earlier than now minus (`expiration` + `--hours` × 60) minutes; if it is null, print the warning "Expiration value not specified in configuration file." The `--hours` grace keeps just-expired rows around briefly, which helps when you want to show a client "your token expired" rather than "unknown token". The command is registered but **not scheduled** for you; add it to `routes/console.php`: ```php Schedule::command('sanctum:prune-expired --hours=24')->daily(); ``` ## A sensible policy for a mobile client - Pass an explicit `expiresAt` per device token (for example 90 days) so each client's lifetime is visible in the row. - Set a global `expiration` as a ceiling for tokens created without one. - Schedule pruning daily so the table does not grow with dead rows. - Revoke on logout with `currentAccessToken()->delete()` rather than waiting for expiry. ## Checklist for an interview answer 1. State the default plainly: `expiration` is `null`, tokens are forever. 2. Name both knobs and their units: minutes from `created_at`, or an absolute `expires_at`. 3. Say that the guard checks both, and that expiry makes the token fail rather than disappear. 4. Mention pruning, its `--hours` grace, and that you must schedule it. 5. Point out that SPA sessions are governed by the session lifetime instead.
- sanctum.expiration is 525600 and a token was created with expiresAt one week ahead. When does it stop working?After one week. The guard requires both the `created_at` window and the `expires_at` timestamp to be valid, so the earlier deadline wins. The config comment suggests the global value overrides `expires_at`, but the pinned guard source joins the two with `and`.
- How would you add an inactivity timeout that Sanctum does not provide?Register `Sanctum::authenticateAccessTokensUsing()` in a service provider. It receives the token and the guard's own verdict, so it can return false when `last_used_at` is older than your idle limit. Keep honouring `$isValid`, or you would revive expired tokens.
saying these in an interview costs you the question
- Sanctum tokens expire after the session lifetime by default
- The global expiration value always overrides a token's expires_at
- An expired Sanctum token row is deleted automatically
- sanctum:prune-expired runs daily as soon as Sanctum is installed
- Setting expiration also logs out users of the first-party SPA