Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Sanctum Laravel Package

laravel/sanctum

Laravel Sanctum provides featherweight authentication for SPAs and simple APIs using token-based and session-based auth. Install it to secure your API routes with minimal configuration.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and scaffold. On Laravel 11+ the fastest path is the first-party installer, which pulls in Sanctum, creates routes/api.php, and wires the api middleware group:

php artisan install:api

On older apps (or if you want the manual route):

composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate

Then give your authenticatable model the trait and contract:

use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens;
}

Make one architectural decision up front. Sanctum is two auth systems in one package, and the biggest day-one mistake is mixing them:

Consumer Mode Credential
Your own SPA on the same top-level domain Cookie/session (stateful) Laravel session cookie + CSRF token
Mobile app, CLI, third-party API, machine-to-machine API tokens Authorization: Bearer <plainTextToken>

Both modes are served by the same auth:sanctum guard, which tries the token first and falls back to the configured session guards — that's why one route file can serve both.

Where to look first.

  1. config/sanctum.php — stateful, guard, expiration, token_prefix, middleware.
  2. Laravel\Sanctum\HasApiTokens — createToken(), tokens(), currentAccessToken(), tokenCan().
  3. Laravel\Sanctum\PersonalAccessToken — the model behind personal_access_tokens, and the Sanctum::usePersonalAccessTokenModel() extension point.

First working use case — a token-protected endpoint:

// routes/api.php
Route::post('/tokens', function (Request $request) {
    $request->validate(['email' => 'required', 'password' => 'required', 'device_name' => 'required']);

    $user = User::where('email', $request->email)->first();

    if (! $user || ! Hash::check($request->password, $user->password)) {
        throw ValidationException::withMessages(['email' => 'Invalid credentials.']);
    }

    return ['token' => $user->createToken($request->device_name)->plainTextToken];
});

Route::middleware('auth:sanctum')->get('/user', fn (Request $r) => $r->user());

The plain-text token is returned once and never recoverable — the database stores a SHA-256 hash of it.

Implementation Patterns

SPA (cookie) flow. Add $middleware->statefulApi() in bootstrap/app.php so EnsureFrontendRequestsAreStateful is applied to the api group, list the SPA origin in SANCTUM_STATEFUL_DOMAINS, then:

axios.defaults.withCredentials = true;
await axios.get('/sanctum/csrf-cookie');   // sets XSRF-TOKEN cookie
await axios.post('/login', credentials);   // normal Laravel session login
await axios.get('/api/user');              // authenticated via session

No tokens are involved; /logout is a normal session invalidate.

Token issuance patterns. One token per device is the canonical pattern — it gives you per-device revocation and readable audit data:

$token = $user->createToken(
    name: $request->device_name,
    abilities: ['posts:read', 'posts:write'],
    expiresAt: now()->addDays(30),   // per-token expiry, independent of config
);

Authorization: two layers. Use abilities as a coarse scope on the token itself, policies for fine-grained rules:

Route::middleware(['auth:sanctum', 'abilities:posts:write'])->group(...);
Route::middleware(['auth:sanctum', 'ability:posts:write,posts:admin'])->group(...); // any-of
if ($request->user()->tokenCan('posts:write')) { /* ... */ }

Ability strings are arbitrary; * is the wildcard. Keep the vocabulary small and stable — abilities are baked into tokens at issuance time, so renaming one silently invalidates every outstanding token that used it.

Revocation and lifecycle. Think about logout as three distinct operations:

$request->user()->currentAccessToken()->delete();   // this device
$request->user()->tokens()->delete();               // all devices (password change, "log out everywhere")
$user->tokens()->where('last_used_at', '<', now()->subMonths(3))->delete(); // hygiene

Hook tokens()->delete() into password-change and account-compromise flows; it's the cheapest security win in the package.

Expiry and pruning. sanctum.expiration is a global TTL in minutes (null = never). Expired tokens are rejected by the guard but remain in the table, so schedule the cleanup command:

Schedule::command('sanctum:prune-expired --hours=24')->daily();

Never-expiring tokens are invisible to that command — pair it with a last_used_at sweep for abandoned devices.

Customising the pipeline. Three extension points cover most real-world needs:

// AppServiceProvider::boot()
Sanctum::usePersonalAccessTokenModel(CompanyToken::class);      // extra columns, tenancy, audit
Sanctum::getAccessTokenFromRequestUsing(                        // non-standard header / query param
    fn ($request) => $request->header('X-Api-Key')
);
Sanctum::$accessTokenAuthenticationCallback = fn ($token, $request) => $token->user; // custom validation

Listen for Laravel\Sanctum\Events\TokenAuthenticated to log usage, enforce IP allow-lists, or detect token replay.

Testing. Sanctum::actingAs() is the whole test story and is dramatically faster than minting real tokens:

Sanctum::actingAs($user, ['posts:write']);   // abilities granted; defaults to ['*']
$this->getJson('/api/posts')->assertOk();

For SPA tests, plain $this->actingAs($user) exercises the session path, which is a genuinely different code path — test both.

Gotchas and Tips

tokenCan() returns true for cookie-authenticated requests. This is the #1 source of broken authorization. A session request has no token, so ability checks pass unconditionally. Never guard SPA routes with tokenCan() or the abilities middleware — use Gates and Policies. Reserve abilities for bearer-token clients.

419 errors are almost always stateful-domain configuration. The origin must match sans scheme and with the port (localhost:5173, not http://localhost:5173), SESSION_DOMAIN must cover both hosts, CORS must set supports_credentials: true with explicit origins (not *), and the client must send withCredentials. Wildcards (*.example.com) work, and any request without an Origin/Referer is treated as non-stateful — which is why raw curl tests against SPA routes behave differently than the browser.

The plain-text token is unrecoverable. Only id|hash lives in the database. Never log it, never email it after signup, and make the client persist it immediately. If a user loses it, issue a new one — there is no "show token again" endpoint to build.

Changing token_prefix or the personal_access_tokens schema invalidates live tokens. The prefix is part of the hashed string; rotating it is a breaking change for every issued token. Ship it before launch or plan a forced re-auth.

last_used_at costs a write per request. It's an UPDATE on every authenticated call, which shows up under load and on read replicas. Sanctum 4.3 added a config toggle to control this tracking — disable it for high-traffic APIs and rely on expires_at instead. Older versions also had Octane and connection-resolution bugs around this column, so keep the package current.

Token cleanup isn't automatic on user deletion. personal_access_tokens uses a polymorphic relation without a foreign key by default; deleted users leave orphans. Cascade manually:

static::deleting(fn ($user) => $user->tokens()->delete());

Middleware order matters. abilities must run after auth:sanctum — on an unauthenticated request it has no token to inspect. If the alias isn't resolving (Class "abilities" does not exist), register it explicitly in bootstrap/app.php with a custom middleware setup.

Config caching traps. SANCTUM_STATEFUL_DOMAINS and the guard list are read from cached config; a stale config:cache after changing domains produces 419s that survive a code deploy. Also verify config/auth.php has 'api' => ['driver' => 'sanctum', ...] and that sanctum.guard contains every guard your SPA can log in through — a second guard (e.g. admin) not listed there means session auth silently fails for it.

Performance hygiene. Sanctum 4.2 added indexes on personal_access_tokens — apply that migration rather than hand-rolling the table, and keep the token lookup keyed by the ID prefix (which the package already does) so lookups stay index-only. Long token names are safe now that the column is text.

Don't over-scope. If you find yourself building refresh tokens, token rotation on every request, or a scopes UI, Sanctum is telling you the requirements have outgrown it — that's Passport territory. Sanctum's value is that the whole surface is ~5 methods and one migration; keep it that way.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo
bernskioldmedia/laravel-harvest
bernskiold/laravel-data-scrubber
bernskiold/laravel-currency-converter
bernskiold/laravel-campaign-monitor
bernskioldmedia/laravel-autotranslate
siberfx/telegram-bot
roy-sandip/laravel-view-extended
gregsphotography/site-builder-core
roy-sandip/laravel-view
anodyne/blade-fluentui-icons
webx-ui/module-catalog-collections