Laravel Magazine

Securing Your Laravel API with Sanctum

Eric Van Johnson · Tutorials
Securing Your Laravel API with Sanctum

Laravel Sanctum is a lightweight authentication package that covers the two most common API authentication scenarios: cookie-based auth for SPAs (Vue, React, Inertia) that live on the same domain, and token-based auth for mobile apps, CLI tools, and third-party integrations. One package, two modes.

This tutorial walks through both, then covers token abilities and expiration, which are the features most developers skip until they regret it.

Installation

Sanctum ships with new Laravel applications. For existing projects:

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

Mode 1: SPA Cookie Authentication

For a single-page application that lives on the same domain or subdomain as your Laravel API, Sanctum uses standard Laravel session cookies rather than tokens. This is the recommended approach for SPAs because it gives you CSRF protection, automatic session rotation, and browser-standard cookie handling.

Configure your stateful domains. In config/sanctum.php, add your SPA's domain:

'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
    '%s%s',
    'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
    Sanctum::currentApplicationUrlWithPort()
))),

Add Sanctum's middleware to your api middleware group in bootstrap/app.php:

->withMiddleware(function (Middleware $middleware) {
    $middleware->statefulApi();
})

SPA login flow:

  1. First, make a GET request to /sanctum/csrf-cookie to get a CSRF cookie
  2. Then POST to your login route with credentials
  3. Laravel sets a session cookie — subsequent requests use it automatically
// Axios example (CSRF is handled automatically with withCredentials)
axios.defaults.withCredentials = true;

await axios.get('/sanctum/csrf-cookie');
await axios.post('/login', { email, password });

// Now authenticated — all subsequent requests use the session cookie
const user = await axios.get('/api/user');

All routes protected with auth:sanctum now use the session cookie for browser-based clients automatically.

Mode 2: API Token Authentication

For mobile apps, CLI tools, and machine-to-machine integrations, Sanctum issues plain-text tokens that are stored hashed in the database.

Add the HasApiTokens trait to your User model:

use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;
}

Issue a token (usually on login):

class AuthController extends Controller
{
    public function login(Request $request): array
    {
        $request->validate([
            'email'       => 'required|email',
            'password'    => 'required',
            'device_name' => 'required|string',
        ]);

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

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

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

The device_name is a label for the token — it shows up in the token list so users can see "iPhone 15 Pro" vs "MacBook Pro" when managing their sessions.

Using the token in requests:

Authorization: Bearer 1|abc123tokenvalue

Protecting routes:

Route::middleware('auth:sanctum')->group(function () {
    Route::get('/user', fn (Request $request) => $request->user());
    Route::apiResource('orders', OrderController::class);
});

Token Abilities — Scope What Each Token Can Do

Plain tokens authenticate the user but do not restrict what they can do. Abilities (Sanctum's term for scopes) let you issue tokens that can only perform specific actions:

// Issue a token with abilities
$token = $user->createToken('mobile-app', ['orders:read', 'orders:create'])->plainTextToken;

// Issue a read-only token for a third-party integration
$readToken = $user->createToken('analytics-integration', ['orders:read'])->plainTextToken;

Check abilities in your controllers or middleware:

public function store(Request $request): Order
{
    if (! $request->user()->tokenCan('orders:create')) {
        abort(403, 'This token does not have permission to create orders.');
    }

    return Order::create($validated);
}

Or use Sanctum's built-in CheckAbilities and CheckForAnyAbility middleware:

Route::post('/orders', OrderController::class)
    ->middleware(['auth:sanctum', 'abilities:orders:create']);

Token Expiration

By default, Sanctum tokens never expire. That is convenient but bad practice. Set an expiration in config/sanctum.php:

'expiration' => 60 * 24 * 30, // 30 days in minutes

Prune expired tokens periodically with:

php artisan sanctum:prune-expired --hours=24

Schedule this in bootstrap/app.php:

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

Revoking Tokens — Let Users Manage Their Sessions

Give users the ability to see and revoke their own tokens. This is the "sign out all devices" feature users expect in modern apps:

class SessionController extends Controller
{
    // List all active tokens
    public function index(Request $request): Collection
    {
        return $request->user()->tokens()->get(['id', 'name', 'last_used_at', 'created_at']);
    }

    // Revoke current token (logout)
    public function destroy(Request $request): void
    {
        $request->user()->currentAccessToken()->delete();
    }

    // Revoke all tokens (logout everywhere)
    public function destroyAll(Request $request): void
    {
        $request->user()->tokens()->delete();
    }
}

Sanctum is the right tool for the majority of Laravel API projects. Use SPA mode for same-domain frontend applications, token mode for anything that needs a shareable credential, and layer on abilities as soon as you have more than one type of client connecting to your API.

Stay Updated

Subscribe to our newsletter

Get latest news, tutorials, community articles and podcast episodes delivered to your inbox.

Weekly articles
We send a new issue of the newsletter every week on Friday.
No spam
We'll never share your email address and you can opt out at any time.