
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Every mobile app, single-page front end, or third-party integration needs a way to prove who is calling your API — and rolling your own token logic is where security bugs breed. Laravel Sanctum authentication gives you a lightweight, first-party way to issue and verify API tokens without the weight of a full OAuth2 server. This guide builds a working token-authenticated REST API on Laravel 11 from scratch: installing Sanctum, issuing a token on login, protecting routes with the auth:sanctum middleware, shaping responses with JSON resources, scoping token abilities, and revoking tokens cleanly. If you would rather have an API designed and hardened for you, see the Laravel API development services.
php artisan install:api to install Sanctum, add the HasApiTokens trait to your User model, issue a token on login with $user->createToken(), and protect your routes with the auth:sanctum middleware. Clients then send the token as a Bearer header.What is Laravel Sanctum and when should you use it?
Laravel Sanctum is Laravel's official package for API authentication. It solves two related problems: issuing long-lived API tokens for mobile apps, CLI tools, and third-party services, and providing cookie-based session authentication for a first-party single-page application (SPA) served from the same domain. It is deliberately simpler than a full OAuth2 server like Passport — no authorization codes, no client secrets, just personal access tokens hashed and stored in your database.
Reach for Sanctum when you control both ends of the connection: your own mobile app, your own Vue or React front end, or a trusted integration. Reach for OAuth2 (Passport) only when third parties need to authorize on behalf of users through a consent screen. For most projects — including the kind of production APIs covered in these Laravel case studies — Sanctum is the right default.
How do you install and configure Laravel Sanctum?
On Laravel 11 and later, Sanctum ships behind a single installer command. It publishes the config, adds the migration for the personal_access_tokens table, and scaffolds the API route file:
composer create-project laravel/laravel sanctum-api
cd sanctum-api
# Installs Sanctum, publishes config, adds routes/api.php and the migration
php artisan install:api
# Create the personal_access_tokens table
php artisan migrate Next, add the HasApiTokens trait to the model that owns tokens — normally App\Models\User. This trait is what gives you the createToken(), tokens(), and currentAccessToken() methods:
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens;
# ... existing casts, fillable, relationships
} That is the entire setup. Unlike Laravel 10, you do not manually register the Sanctum middleware in a Kernel class — install:api wires routes/api.php into bootstrap/app.php for you, and the auth:sanctum guard is available immediately.
How do you issue an API token on login?
Token issuing lives in a controller. Validate the credentials, confirm the password with Hash::check, then call createToken() on the authenticated user. The method returns a NewAccessToken object whose plainTextToken is shown to the client exactly once — only a SHA-256 hash is stored in the database.
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
public function login(Request $request)
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
$user = User::where('email', $credentials['email'])->first();
if (! $user || ! Hash::check($credentials['password'], $user->password)) {
throw ValidationException::withMessages([
'email' => ['The provided credentials are incorrect.'],
]);
}
# 'auth_token' is a label; the array is the token's abilities
$token = $user->createToken('auth_token', ['posts:read'])->plainTextToken;
return response()->json([
'access_token' => $token,
'token_type' => 'Bearer',
]);
}
} Register the route in routes/api.php. Everything in that file is automatically prefixed with /api, so this endpoint is reachable at POST /api/login:
use App\Http\Controllers\Api\AuthController;
use Illuminate\Support\Facades\Route;
Route::post('/login', [AuthController::class, 'login']); How do you protect routes with the auth:sanctum middleware?
Any route wrapped in the auth:sanctum middleware requires a valid token. Sanctum reads the Authorization: Bearer <token> header, hashes the value, looks it up in personal_access_tokens, and resolves the owning user onto the request. A missing or invalid token returns 401 Unauthenticated automatically.
use App\Http\Controllers\Api\AuthController;
use App\Http\Controllers\Api\PostController;
use Illuminate\Support\Facades\Route;
Route::post('/login', [AuthController::class, 'login']);
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', fn (Request $request) => $request->user());
Route::apiResource('posts', PostController::class);
Route::post('/logout', [AuthController::class, 'logout']);
}); Inside any protected controller, $request->user() (or the auth()->user() helper) returns the authenticated model — the same API you already use for web sessions.
auth:sanctum middleware: the Bearer token is hashed, matched against the personal_access_tokens table, and either resolves the user or returns a 401.Returning clean JSON with API resources
Do not return raw Eloquent models — they leak every column, including timestamps and internal flags. An API resource is a transformer that defines the exact JSON shape a client sees:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'body' => $this->body,
'author' => $this->user->name,
'created_at' => $this->created_at->toIso8601String(),
];
}
} # In the controller — collection and single-resource responses
public function index()
{
return PostResource::collection(
Post::with('user')->latest()->paginate(15)
);
}
public function show(Post $post)
{
return new PostResource($post->load('user'));
} How do token abilities and scopes limit access?
Abilities (also called scopes) let a single token carry a restricted set of permissions. A read-only mobile client can hold a token that can fetch posts but never delete them, while an admin token holds broader abilities. You assign abilities as the second argument to createToken() and check them per request.
# Issue tokens with different ability sets
$readToken = $user->createToken('mobile', ['posts:read'])->plainTextToken;
$adminToken = $user->createToken('admin', ['posts:read', 'posts:delete'])->plainTextToken; Enforce an ability inside a controller with tokenCan(), or gate an entire route with the abilities / ability middleware:
# Per-action check inside a controller
public function destroy(Request $request, Post $post)
{
if (! $request->user()->tokenCan('posts:delete')) {
abort(403, 'This token cannot delete posts.');
}
$post->delete();
return response()->noContent();
}
# Or guard the route directly (all listed abilities required)
Route::delete('/posts/{post}', [PostController::class, 'destroy'])
->middleware(['auth:sanctum', 'abilities:posts:delete']); - abilities middleware — the token must have all listed abilities.
- ability middleware — the token needs any one of the listed abilities.
- A token created with
['*']passes every ability check — reserve it for fully trusted first-party clients.
How do you revoke Sanctum tokens on logout?
Because tokens are database rows, revoking one is a delete. On logout, delete the token that made the current request; for a "log out everywhere" feature, delete all of the user's tokens.
public function logout(Request $request)
{
# Revoke only the token used for this request
$request->user()->currentAccessToken()->delete();
return response()->json(['message' => 'Logged out.']);
}
# Revoke every token the user holds (log out all devices)
$request->user()->tokens()->delete(); For long-lived tokens, set an expiry with the expiration value in config/sanctum.php (in minutes) and schedule sanctum:prune-expired to clear stale rows. Short-lived tokens plus refresh-on-login is a sound default for mobile apps.
Should you use Sanctum token auth or SPA cookie auth?
Sanctum offers two distinct modes, and picking the wrong one causes most of the confusion around it. API token authentication suits any client that can store a token and send it as a header — mobile apps, desktop apps, server-to-server calls, and CLIs. SPA authentication uses stateful, encrypted session cookies plus CSRF protection, and is meant only for a first-party JavaScript front end served from the same top-level domain as the API.
Do not mix the two for the same client. A mobile app should never use cookie mode, and a same-domain SPA gains nothing from storing tokens in JavaScript (where they are exposed to XSS). Once the API works, automate its deployment — the same discipline as this GitLab CI/CD pipeline for Laravel keeps token migrations and config in sync across environments, and the caching tips in this Laravel performance optimization guide keep authenticated endpoints fast under load.
Conclusion
Building a REST API with Laravel Sanctum authentication comes down to a short, well-worn path: install with install:api, add HasApiTokens, issue tokens on login, guard routes with auth:sanctum, shape output with resources, scope tokens with abilities, and revoke by deleting rows. Choose token mode for apps and integrations, cookie mode for same-domain SPAs, and never mix them. That combination gives you an API that is secure by default and simple to reason about. If you want a production-grade Laravel Sanctum API architected, tested, and deployed for you, get in touch to discuss the project.