PHPackages                             d076/sanctum-refresh-tokens - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Authentication &amp; Authorization](/categories/authentication)
4. /
5. d076/sanctum-refresh-tokens

ActiveLibrary[Authentication &amp; Authorization](/categories/authentication)

d076/sanctum-refresh-tokens
===========================

Refresh token realization for laravel sanctum

v4.0.0(2mo ago)64.8k↑213.1%MITPHPPHP ^8.3CI passing

Since Feb 29Pushed 2mo ago1 watchersCompare

[ Source](https://github.com/D076/sanctum-refresh-tokens)[ Packagist](https://packagist.org/packages/d076/sanctum-refresh-tokens)[ RSS](/packages/d076-sanctum-refresh-tokens/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (7)Dependencies (16)Versions (8)Used By (0)

Laravel Sanctum Refresh Tokens
==============================

[](#laravel-sanctum-refresh-tokens)

[![Tests](https://github.com/d076/sanctum-refresh-tokens/actions/workflows/tests.yml/badge.svg)](https://github.com/d076/sanctum-refresh-tokens/actions/workflows/tests.yml)[![Latest Version](https://camo.githubusercontent.com/4082053aa22aa9c63f1db883914ab90ea754d7eff24b540288b375998e2f2079/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f643037362f73616e6374756d2d726566726573682d746f6b656e732e737667)](https://packagist.org/packages/d076/sanctum-refresh-tokens)[![License](https://camo.githubusercontent.com/65813ca9a91171f54e6da0259a0ed6d249ee4fadba9b5565b613fabf982e2daf/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f643037362f73616e6374756d2d726566726573682d746f6b656e732e737667)](LICENSE)

Refresh tokens on top of [Laravel Sanctum](https://laravel.com/docs/sanctum). Sanctum issues long-lived personal access tokens; this package adds a short-lived **access token** paired with a longer-lived, single-use **refresh token**, so a client can silently obtain a fresh pair without re-authenticating — the standard pattern for SPAs and mobile apps.

Features
--------

[](#features)

- **Access + refresh token pairs** issued together, each with its own TTL.
- **Single-use rotation** — exchanging a refresh token deletes it and its bound access token, then issues a brand-new pair. Replaying a used token fails.
- **Hashed at rest** — refresh tokens are stored as SHA-256 hashes and compared in constant time (`hash_equals`); the plaintext is only ever returned to the client.
- **TTL enforced on lookup** — expired refresh tokens are never matched.
- **Credential login, logout and password reset** helpers that revoke the right tokens.
- **Override-friendly** — no routes or controllers are shipped; you wire your own. Services are bound behind interfaces, and the user model's email/password fields are configurable.
- **Prune command** for housekeeping expired tokens.

Requirements
------------

[](#requirements)

VersionPHP`^8.3`Laravel`12`, `13`Sanctum`^4.0`Tested against PHP 8.3 / 8.4 / 8.5 and Laravel 12 / 13 on SQLite, PostgreSQL and MySQL.

Installation
------------

[](#installation)

```
composer require d076/sanctum-refresh-tokens
```

Publish and run the migration (creates the `personal_refresh_tokens` table):

```
php artisan vendor:publish --tag=sanctum-refresh-tokens
php artisan migrate
```

This package builds on Sanctum's `personal_access_tokens` table, so make sure Sanctum itself is installed and migrated (`php artisan install:api` on a fresh app).

### Upgrading from 3.x

[](#upgrading-from-3x)

4.0 adds an `abilities` column to `personal_refresh_tokens` (so a token's scope is preserved across refreshes). Re-publish and migrate to pick it up:

```
php artisan vendor:publish --tag=sanctum-refresh-tokens
php artisan migrate
```

Refresh tokens issued before the upgrade have no stored scope and fall back to `['*']`on their next refresh.

Setup
-----

[](#setup)

Extend your authenticatable model from `AuthenticatableUser`:

```
use D076\SanctumRefreshTokens\Models\AuthenticatableUser;

class User extends AuthenticatableUser
{
    // ...
}
```

`AuthenticatableUser` already pulls in Sanctum's `HasApiTokens` plus this package's refresh-token behaviour. If you can't change your base class, use the trait directly and implement the contract instead:

```
use D076\SanctumRefreshTokens\HasApiTokensInterface;
use D076\SanctumRefreshTokens\Traits\HasApiTokens;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable implements HasApiTokensInterface
{
    use HasApiTokens;
}
```

Configuration
-------------

[](#configuration)

Token lifetimes are read from Sanctum's config (`config/sanctum.php`). Add the keys this package uses alongside Sanctum's own:

```
// config/sanctum.php
'expiration' => env('SANCTUM_ACCESS_TOKEN_EXPIRATION', 60),                          // access token, minutes
'refresh_token_expiration' => env('SANCTUM_REFRESH_TOKEN_EXPIRATION', 43200),        // refresh "remember me", minutes (30 days)
'refresh_token_expiration_no_remember' => env('SANCTUM_REFRESH_TOKEN_EXPIRATION_NO_REMEMBER', 1440), // refresh without remember, minutes (1 day)
```

If a key is absent the built-in defaults above are used, so the package works out of the box. The optional `sanctum.token_prefix` is honoured for refresh tokens too (useful for secret-scanning).

Usage
-----

[](#usage)

The package ships **no routes or controllers** — you stay in control of your API surface. Inject the services where you need them.

### Issuing tokens

[](#issuing-tokens)

```
use D076\SanctumRefreshTokens\Services\TokenService;

$tokens = (new TokenService($user))->createTokens();
```

`createTokens()` accepts optional overrides:

```
$tokens = (new TokenService($user))->createTokens(
    accessTokenExpiresAt: now()->addMinutes(15),
    refreshTokenExpiresAt: now()->addDays(7),
    abilities: ['orders:read', 'orders:write'],
);
```

It returns a `TokensDTO`:

```
$tokens->access_token;              // plain-text bearer token
$tokens->refresh_token;             // plain-text refresh token ("{id}|{token}")
$tokens->token_type;                // "Bearer"
$tokens->access_token_expires_at;   // Carbon|null
$tokens->refresh_token_expires_at;  // Carbon|null
$tokens->model;                     // morph class of the user
$tokens->user;                      // the user model

return response()->json($tokens);   // implements Arrayable
```

### Logging in with credentials

[](#logging-in-with-credentials)

```
use D076\SanctumRefreshTokens\DTOs\LoginDTO;
use D076\SanctumRefreshTokens\Services\IAuthService;

public function login(Request $request, IAuthService $auth)
{
    $credentials = new LoginDTO(
        email: $request->input('email'),
        password: $request->input('password'),
        model: User::class,
        remember: $request->boolean('remember'),
    );

    // throws Illuminate\Auth\AuthenticationException on bad credentials
    $tokens = $auth->setCredentials($credentials)->login();

    return response()->json($tokens);
}
```

`remember` selects between the two refresh-token TTLs (`refresh_token_expiration`vs `refresh_token_expiration_no_remember`).

Already have the user (e.g. social login)? Skip credentials:

```
$tokens = $auth->setUser($user)->login();
```

### Refreshing

[](#refreshing)

```
public function refresh(Request $request, IAuthService $auth)
{
    // throws AuthenticationException if the token is unknown, expired or already used
    $tokens = $auth->refresh($request->input('refresh_token'));

    return response()->json($tokens);
}
```

The old refresh token **and its bound access token** are deleted before the new pair is issued, so a stolen-and-replayed token is rejected on the second use.

### Logout

[](#logout)

Behind the `auth:sanctum` guard, the authenticated request carries the current access token, so logout can revoke exactly that pair:

```
public function logout(Request $request, IAuthService $auth)
{
    $auth->setUser($request->user())->logout();

    return response()->noContent();
}
```

### Password reset

[](#password-reset)

Hashes the new password, saves it, and revokes **all** of the user's access and refresh tokens:

```
$auth->setUser($user)->resetPassword($newPassword);
```

### Revoking tokens directly

[](#revoking-tokens-directly)

```
use D076\SanctumRefreshTokens\Services\TokenService;

(new TokenService($user))->deleteCurrentTokens(); // current pair only
(new TokenService($user))->deleteAllTokens();      // every pair
```

Pruning expired tokens
----------------------

[](#pruning-expired-tokens)

A console command removes refresh tokens that expired more than `--hours` ago (default 24):

```
php artisan sanctum:prune-refresh-expired --hours=0
```

Schedule it next to Sanctum's own pruning:

```
use Illuminate\Support\Facades\Schedule;

Schedule::command('sanctum:prune-expired --hours=0')->hourly();
Schedule::command('sanctum:prune-refresh-expired --hours=0')->daily();
```

Customisation
-------------

[](#customisation)

### Custom email / password columns

[](#custom-email--password-columns)

If your model doesn't use `email` / `password`, expose the column names and the package will pick them up:

```
class User extends AuthenticatableUser
{
    public function getEmailField(): string
    {
        return 'login';
    }

    public function getPasswordField(): string
    {
        return 'pass_hash';
    }
}
```

### Swapping the service implementations

[](#swapping-the-service-implementations)

Both services are bound behind interfaces, so you can rebind your own in a service provider:

```
use D076\SanctumRefreshTokens\Services\IAuthService;
use D076\SanctumRefreshTokens\Services\ITokenService;

$this->app->bind(IAuthService::class, MyAuthService::class);
$this->app->bind(ITokenService::class, MyTokenService::class);
```

How it works
------------

[](#how-it-works)

- A refresh token is `{id}|{token}`, where `{token}` is 40 random characters plus a CRC32b checksum (and the optional `sanctum.token_prefix`).
- Only `SHA-256({token})` is stored in `personal_refresh_tokens.token`; the column is hidden from serialization.
- `PersonalRefreshToken::findToken()` looks up the row by id, verifies the hash with `hash_equals()`, and only matches rows whose `expires_at` is in the future.
- Each refresh token is linked to the access token it was issued with (`access_token_id`); deleting the refresh token cascades to that access token via a model observer.
- The token's abilities (scope) are stored on the refresh token row, so refreshing preserves the scope even after the short-lived access token has been pruned.

Testing
-------

[](#testing)

```
composer install
composer test      # Pest
composer analyse   # PHPStan (level 6)
```

A Docker setup is included to run the suite (including the PostgreSQL/MySQL matrix):

```
docker compose run --rm test composer install
docker compose run --rm test vendor/bin/pest

# cross-database
docker compose up -d --wait pgsql mysql
docker compose run --rm -e DB_DRIVER=pgsql test vendor/bin/pest --group=cross-db
docker compose run --rm -e DB_DRIVER=mysql test vendor/bin/pest --group=cross-db
```

Changelog &amp; License
-----------------------

[](#changelog--license)

See [CHANGELOG.md](CHANGELOG.md). Released under the [MIT license](LICENSE).

###  Health Score

50

—

FairBetter than 95% of packages

Maintenance86

Actively maintained with recent releases

Popularity28

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity62

Established project with proven stability

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~138 days

Recently: every ~203 days

Total

7

Last Release

72d ago

Major Versions

v1.0.1 → v2.0.02024-03-14

v2.0.1 → v3.0.02024-08-12

v3.1.0 → v4.0.02026-06-07

PHP version history (2 changes)v1.0.0PHP ^8.2

v4.0.0PHP ^8.3

### Community

Maintainers

![](https://www.gravatar.com/avatar/c9625f3deb9354a1834ef911763c1ee6598527b532c34cd69d62495e3bbcb411?d=identicon)[D076](/maintainers/D076)

---

Top Contributors

[![D076](https://avatars.githubusercontent.com/u/33459398?v=4)](https://github.com/D076 "D076 (8 commits)")

---

Tags

laravelauthsanctum

###  Code Quality

TestsPest

Static AnalysisPHPStan

### Embed Badge

![Health badge](/badges/d076-sanctum-refresh-tokens/health.svg)

```
[![Health](https://phpackages.com/badges/d076-sanctum-refresh-tokens/health.svg)](https://phpackages.com/packages/d076-sanctum-refresh-tokens)
```

###  Alternatives

[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.6k31.8M162](/packages/laravel-cashier)[laravel/ai

The official AI SDK for Laravel.

1.1k4.6M322](/packages/laravel-ai)[spatie/laravel-health

Monitor the health of a Laravel application

88212.7M188](/packages/spatie-laravel-health)[spatie/laravel-permission

Permission handling for Laravel 12 and up

13.0k107.5M1.6k](/packages/spatie-laravel-permission)[api-platform/laravel

API Platform support for Laravel

58190.1k21](/packages/api-platform-laravel)[psalm/plugin-laravel

Psalm plugin for Laravel

3345.4M354](/packages/psalm-plugin-laravel)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
