d076/sanctum-refresh-tokens
Add refresh token support to Laravel Sanctum. Issue short-lived access tokens alongside revocable refresh tokens, rotate them on use, and keep API authentication secure without long-lived tokens.
Requirements first — this is a hard gate. v4.0.0 requires PHP 8.3+ and Laravel ^12.0|^13.0. Laravel 11 and PHP 8.2 are no longer supported. If you are on Laravel 11, stay on 3.1.x or plan the framework upgrade as part of adopting this release.
composer require d076/sanctum-refresh-tokens — confirm the exact vendor/name against the README install block) and let Laravel package discovery register the service provider.personal_refresh_tokens table, including the new abilities column added in 4.0. Run the migration before deploying the new code — the ability carry-over logic depends on the column existing.AuthService — specifically refresh() and getUserFromCredentials(). Those two methods are the whole contract you will build against.User model: if you use the package's AuthenticatableUser base class, it extends Illuminate\Foundation\Auth\User; if you have your own model, make sure it satisfies the same contract.First use case: wire up login → refresh → logout. On login, authenticate credentials and return a short-lived access token plus a long-lived refresh token. On refresh, hand the refresh token to AuthService::refresh(), get a new pair back, and replace what the client stores. Protect your normal API routes with Sanctum's auth:sanctum; protect the refresh endpoint with throttling instead (the client's access token is expired by definition).
Token pair on login. Issue the access token with the narrowest abilities the client needs. Because abilities are now persisted on the refresh token, whatever you pass at login is what you get back forever after — this is the intended design, so decide scopes at issuance rather than trying to widen them later.
Refresh with rotation. Treat every refresh as a swap: persist the newly returned refresh token and discard the old one. The old token is invalidated by rotation, so a client that keeps using the stale one will get rejected. Store refresh tokens in an HttpOnly, Secure, SameSite cookie where possible; keep access tokens in memory on the client.
Handle replay as a security event, not a retry. If a refresh token that has already been rotated is presented again, the package's replay protection will reject it. Map that failure to a 401, clear the client's cookie, and force a full re-login. Log the event — repeated replays usually mean a leaked token.
Logout and password reset = revocation. On logout, revoke the refresh token (and ideally the current access token). On password reset, revoke all refresh tokens for the user so existing sessions cannot silently extend themselves. The 4.0 test suite covers both flows, so mirror its expectations in your own feature tests.
Custom email columns now work. getUserFromCredentials() resolves getEmailField() on a model instance in 4.0. If you authenticate by something other than email, this is the release that stops fataling — add a regression test for your column.
Test with time travel. TTL enforcement is best tested with Laravel's travel()/Carbon::setTestNow(): issue a pair, travel past the access TTL, assert refresh still works, travel past the refresh TTL, assert it fails. Also assert the DB stores a hash, not the raw token.
Upgrade pattern from 3.x: bump PHP and Laravel constraints, run the new migration, then decide what to do with refresh tokens issued under 3.x. Their abilities were previously not persisted, so treat pre-upgrade tokens as untrusted — the cleanest path is to invalidate them and require one re-login. A backfill is possible but you would be guessing at the original scope, which is exactly the bug 4.0 fixes.
['*']. If you shipped scoped tokens on 3.x, assume they were silently widened and audit any sensitive action those tokens could reach. Revoke refresh tokens issued before the upgrade.laravel/framework is now a direct dependency. This makes installs more honest but can conflict with apps that pin the framework tightly, and it means the package can no longer be used in an illuminate-only, framework-less setup. Resolve conflicts at the root composer.json.refresh() writes to a column that does not exist.abilities column directly, verify the column type on your engine (JSON vs. text) before writing whereJsonContains-style queries.User model are fully validated; run Larastan in your own app against your subclass.AuthenticatableUser for custom user behaviour, and use the notifications dependency (illuminate/notifications) to brand password-reset emails. Publish any package config before overriding, so future upgrades do not clobber your changes.personal_refresh_tokens rows for the user; the stored abilities and revocation state usually explain the 401 immediately.^4.0 in composer.json keeps you from being surprised by a future major.How can I help you explore Laravel packages today?