Laravel Passport access tokens last a year by default — how do tokensExpireIn and its siblings work, why is expires_at display-only, and what does passport:purge remove?
answer
- P1Y unless configured
- four lifetime setters in boot()
- expiry lives inside the token
- revoke(), not editing expires_at
- --revoked, --expired, --hours=168
basics
~10 sPassport::tokensExpireIn(), refreshTokensExpireIn() and personalAccessTokensExpireIn() each default to one year; clientCredentialsTokensExpireIn() falls back to tokensExpireIn. Expiry is baked into the signed token, so expires_at is display-only; revoke to invalidate. passport:purge deletes revoked and long-expired rows.
solid answer
~40 sLifetimes are set in `AppServiceProvider::boot()`: `Passport::tokensExpireIn()` for access tokens, `refreshTokensExpireIn()`, `personalAccessTokensExpireIn()` — each defaults to `P1Y` — and `clientCredentialsTokensExpireIn()`, which falls back to `tokensExpireIn()` when unset. Authorization codes last ten minutes. The expiry is written **inside** the signed access token, so the `expires_at` columns are read-only and for display; editing them changes nothing, and a new lifetime applies only to tokens issued afterwards. To stop a token early you **revoke** it: the resource server checks the `oauth_access_tokens` row on every request and refuses revoked (or missing) rows. `php artisan passport:purge` deletes revoked tokens, auth codes, refresh tokens and device codes plus those expired for over `--hours` (default 168); `--revoked` or `--expired` narrow it. Schedule it yourself.
code
php · 18 lines<?php
namespace App\Providers;
use Carbon\CarbonInterval;
use Illuminate\Support\ServiceProvider;
use Laravel\Passport\Passport;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Passport::tokensExpireIn(CarbonInterval::hours(1));
Passport::refreshTokensExpireIn(CarbonInterval::days(30));
Passport::clientCredentialsTokensExpireIn(CarbonInterval::minutes(30));
Passport::personalAccessTokensExpireIn(CarbonInterval::months(6));
}
}go deeper
Remember the one-year default and that tokensExpireIn, refreshTokensExpireIn and personalAccessTokensExpireIn are set in a service provider's boot method.
Explain that expiry is inside the signed token, why expires_at is display-only, and how revocation is checked on each request.
Set short access-token lifetimes with refresh tokens, handle leaks by revocation or key rotation, and schedule passport:purge.
Balance integrator friction against exposure: token lifetimes, refresh rotation and purge retention are policy as much as configuration.
## Four lifetimes, one default Passport exposes static setters, called in `AppServiceProvider::boot()`, that accept a `DateInterval` (such as `CarbonInterval::days(15)`) or a `DateTimeInterface`: | Setter | Applies to | Default | |---|---|---| | `Passport::tokensExpireIn()` | access tokens from authorization code, refresh, password, implicit and device grants | one year (`P1Y`) | | `Passport::refreshTokensExpireIn()` | refresh tokens | one year | | `Passport::personalAccessTokensExpireIn()` | tokens from `$user->createToken()` | one year | | `Passport::clientCredentialsTokensExpireIn()` | client credentials tokens | falls back to `tokensExpireIn()` | Authorization codes are fixed at ten minutes by the service provider. The docs describe the defaults as **long-lived**; for integrators on a logistics platform, short access tokens (minutes to hours) plus refresh tokens are the usual choice, because a leaked access token then expires quickly on its own. ## Why `expires_at` is display-only Passport's access tokens are **signed by the private key**, and the expiry is one of the values inside them; refresh tokens and auth codes are encrypted payloads that carry their own expiry too. The database row is a record, not the source of truth. Consequences: - Changing `expires_at` in `oauth_access_tokens` neither extends nor shortens a token. - Calling `tokensExpireIn()` with a shorter interval affects only **newly issued** tokens; year-long tokens already handed out stay valid until their own expiry. - To cut a token short, **revoke** it. ## Revocation is a database check The resource server asks Passport's access-token repository whether a token is revoked on every request, and the repository answers "revoked" unless a row with that id exists with `revoked = false`. So: 1. `$token->revoke()` (or `$request->user()->token()->revoke()`) sets `revoked = true` and the next request fails. 2. Deleting a token's row has the same effect. 3. Revoking an access token does not revoke its refresh token; revoke that too, or the client can mint a fresh access token. ## `passport:purge` The command's signature is `passport:purge {--revoked} {--expired} {--hours=168}`: - with no flags it deletes rows that are **revoked** or **expired for more than `--hours`** (seven days by default) from access tokens, auth codes, refresh tokens and, when the device grant is enabled, device codes; - `--revoked` deletes only revoked rows; - `--expired` deletes only rows expired beyond the retention window. Nothing schedules it for you. The docs suggest: ```php Schedule::command('passport:purge')->hourly(); ``` ## A lifetime policy for integrators - Access tokens: short (for example one hour) via `tokensExpireIn()`. - Refresh tokens: longer (for example 30 days) via `refreshTokensExpireIn()`; Passport revokes a refresh token after use by default, so each refresh returns a new one. - Client credentials: set explicitly, since machines can simply request a new token. - Personal access tokens: set a finite lifetime instead of a year. - Purge on a schedule so the token tables stay small. ## Why the database still matters for "stateless" tokens Passport's access tokens are self-contained and signed, which tempts people to call them stateless. In Passport they are not fully stateless: every validation checks the `oauth_access_tokens` row for revocation. That is what makes `revoke()` effective immediately, and it is also why deleting token rows by hand has the same effect as revoking them. The trade-off is one indexed lookup per API request. ## Common mistakes 1. Relying on the one-year default for third-party integrators. 2. Shortening `tokensExpireIn()` after an incident and assuming existing tokens died with the change. 3. Revoking an access token but leaving its refresh token valid. 4. Never scheduling `passport:purge`, so revoked and expired rows accumulate for years.
- You shorten tokensExpireIn to one hour after a leak. Are the year-long tokens already issued now invalid?No. Each access token carries its own expiry inside the signed token, so the new setting applies only to tokens issued from now on. To kill existing tokens, revoke them and their refresh tokens, or rotate the signing keys to invalidate every access token at once (refresh tokens still need revoking separately).
- Does passport:purge ever remove a token that is still usable?No. It deletes only rows that are revoked or expired beyond the retention window. Because the resource server treats a missing row as revoked, deleting a live token's row would revoke it, but purge's conditions never select one.
saying these in an interview costs you the question
- Passport access tokens expire after the session lifetime by default
- Editing expires_at in oauth_access_tokens extends a token
- Changing tokensExpireIn immediately shortens existing tokens
- Passport tokens are fully stateless, so revocation is impossible
- passport:purge runs automatically once Passport is installed