Laravel Sanctum is the official, lightweight API authentication package built into Laravel. It handles token issuance, route protection, and token revocation with minimal setup — making it the right choice for almost every Laravel REST API and mobile backend.
This tutorial covers a complete Sanctum setup from scratch: installation, User model configuration, protected routes, a full AuthController, and how to make authenticated requests. All examples work with Laravel 12.
What is Laravel Sanctum?
Sanctum provides API token authentication using plain-text tokens stored in a personal_access_tokens database table. Unlike JWT, tokens aren't self-contained — each request does a quick database lookup to validate. The trade-off is instant, reliable token revocation.
| Feature | Sanctum | Passport | JWT (tymon) |
|---|---|---|---|
| Best for | Your own API | OAuth2 / third-party | Stateless / microservices |
| Token storage | Database | Database | Client-side only |
| Setup complexity | Simple | Complex | Medium |
| Built into Laravel | ✓ Yes | ✕ No | ✕ No |
| Token revocation | Easy | ✓ Yes | Hard |
| SPA cookie auth | ✓ Yes | ✕ No | ✕ No |
For most Laravel REST APIs, Sanctum is the right choice. Use Passport only when you need a full OAuth2 server for third-party developers.
Step 1: Install Laravel Sanctum
On Laravel 11 and 12, Sanctum is included by default. For Laravel 10 and below, install it manually:
# Laravel 10 and below — install manually
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
Laravel 11 / 12: Sanctum is already installed. Just run php artisan migrate if you haven't already — the personal_access_tokens table migration is included.
Step 2: Add HasApiTokens to the User Model
Add the HasApiTokens trait to your User model. This gives the model the createToken(), tokens(), and currentAccessToken() methods:
app/Models/User.php
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens;
// rest of your model...
}
Step 3: Set Up API Routes
Define your public and protected routes in routes/api.php. The auth:sanctum middleware guards any route that requires a valid token:
routes/api.php
use App\Http\Controllers\Api\AuthController;
use App\Http\Controllers\Api\UserController;
Route::prefix('v1')->group(function () {
// Public routes — no token required
Route::prefix('auth')->group(function () {
Route::post('register', [AuthController::class, 'register']);
Route::post('login', [AuthController::class, 'login']);
});
// Protected routes — valid Sanctum token required
Route::middleware('auth:sanctum')->group(function () {
Route::post('auth/logout', [AuthController::class, 'logout']);
Route::get('auth/me', [AuthController::class, 'me']);
Route::get('users', [UserController::class, 'index']);
});
});
Step 4: Build the AuthController
This controller handles registration, login, logout, and the authenticated user endpoint. The key line is createToken()->plainTextToken — this generates and returns the token the client will store and send on future requests:
app/Http/Controllers/Api/AuthController.php
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller
{
public function register(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8|confirmed',
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json([
'user' => $user,
'token' => $token,
], 201);
}
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
]);
if (! Auth::attempt($request->only('email', 'password'))) {
return response()->json([
'message' => 'Invalid credentials'
], 401);
}
$user = Auth::user();
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json([
'user' => $user,
'token' => $token,
]);
}
public function logout(Request $request)
{
$request->user()->currentAccessToken()->delete();
return response()->json([
'message' => 'Logged out successfully'
]);
}
public function me(Request $request)
{
return response()->json($request->user());
}
}
Step 5: Make Authenticated Requests
After login, the client receives a plain-text token. Send it in the Authorization header as a Bearer token on every protected request:
HTTP Requests
# 1. Login — get your token
POST /api/v1/auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "yourpassword"
}
# Response
{
"user": { "id": 1, "name": "John", ... },
"token": "1|abc123yourtokenhere..."
}
# 2. Use the token on protected requests
GET /api/v1/auth/me
Authorization: Bearer 1|abc123yourtokenhere...
Token format: Sanctum tokens look like 1|randomstring. The number before the pipe is the token ID in the database. Always send the full string including the numeric prefix.
Token Abilities (Optional Scopes)
Sanctum supports token abilities — a simple alternative to OAuth scopes. You can restrict what each token is allowed to do:
Token abilities
// Issue a token with specific abilities
$token = $user->createToken('mobile-app', ['read', 'write'])->plainTextToken;
// Check ability in a controller
if ($request->user()->tokenCan('write')) {
// allowed
}
// Or enforce via middleware
Route::middleware(['auth:sanctum', 'ability:write'])->group(function () {
Route::post('posts', [PostController::class, 'store']);
});
Revoking Tokens
Because tokens are stored in the database, revocation is instant. This is one of Sanctum's biggest advantages over JWT — no denylist needed, just delete the row:
Revocation options
// Revoke the current token (standard logout)
$request->user()->currentAccessToken()->delete();
// Revoke all tokens — logout from every device
$user->tokens()->delete();
// Revoke tokens by name
$user->tokens()->where('name', 'mobile-app')->delete();
Setting Token Expiry
Set the expiration value in config/sanctum.php. The value is in minutes:
config/sanctum.php
// Expire tokens after 60 days
'expiration' => 60 * 24 * 60,
// Tokens that never expire
'expiration' => null,
Common Issues & Fixes
401 Unauthenticated on every request
Make sure you're sending the token as Authorization: Bearer YOUR_TOKEN — not as a query parameter or cookie. Also confirm the HasApiTokens trait is on your User model and migrations have run.
CORS errors from a frontend SPA
Add your frontend URL to config/cors.php:
config/cors.php
'allowed_origins' => ['https://yourfrontend.com'],
'supports_credentials' => true,
Missing personal_access_tokens table
Run php artisan migrate and confirm the table exists in your database. On Laravel 11/12 this migration is included automatically.
Frequently Asked Questions
Does Laravel Sanctum use JWT tokens?
No. Sanctum uses plain-text tokens stored in your database, not JWT. JWT tokens are stateless and self-contained; Sanctum tokens require a database lookup on each request. The trade-off is that Sanctum tokens are much easier to revoke instantly. See the full breakdown in Laravel Sanctum vs JWT.
When should I use Sanctum vs Passport?
Use Sanctum when you're building an API for your own frontend — SPA or mobile app. Use Passport only when you need a full OAuth2 server to issue tokens to third-party developers. Passport is significantly more complex to set up and maintain.
Can I use Sanctum for SPA cookie authentication?
Yes — Sanctum also supports cookie-based SPA authentication using CSRF tokens. For a REST API or mobile backend, token-based authentication as shown in this tutorial is the standard approach.
Work with me
Need help with your Laravel architecture?
I build clean, maintainable Laravel backends for SaaS products — REST APIs, queue systems and production patterns like the one in this article.