Building Secure RESTful APIs with Laravel Sanctum
Complete step-by-step guide to building secure REST APIs using Laravel Sanctum with authentication, authorization, and frontend integration examples.
StalkTechie
Author
Building Secure RESTful APIs with Laravel Sanctum
Laravel Sanctum provides a lightweight way to authenticate single-page applications (SPAs), mobile apps, and token-based APIs. Unlike Passport (which is OAuth2-heavy), Sanctum uses Laravel's built-in session authentication for SPAs and API tokens for other clients. This guide walks through setup, authentication, authorization, rate limiting, testing, and integration with frontends like Vue.js or React.
Installation and Configuration
Install Sanctum via Composer and publish its config.
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
Add Sanctum's middleware and traits to your app.
// app/Http/Kernel.php
protected $middlewareGroups = [
'api' => [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
// app/Models/User.php
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
}
Config in config/sanctum.php: Set token expiration, stateful domains (e.g., for SPAs on localhost:3000).
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,127.0.0.1')),
'expiration' => 525600, // 1 year in minutes
Add to .env: SANCTUM_STATEFUL_DOMAINS=localhost:3000.
Issuing API Tokens
For token-based auth (non-SPA clients like mobile apps).
Creating Tokens
// In a controller or tinker
$user = User::find(1);
$token = $user->createToken('mobile-app-token', ['product:create', 'product:read']); // Abilities (scopes)
return response()->json(['token' => $token->plainTextToken]);
Tokens are stored in personal_access_tokens table. Revoke: $user->tokens()->delete();.
Plain Text Tokens
Sanctum tokens are hashed in DB but returned plain once. Structure: {id}|{random}.
Authentication in Routes
Protect API routes with sanctum guard.
// routes/api.php
use Illuminate\Http\Request;
use App\Http\Controllers\ProductController;
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
Route::middleware(['auth:sanctum', 'abilities:product:read'])->apiResource('products', ProductController::class);
Client sends token in header: Authorization: Bearer {token}.
Abilities (Scopes)
Check specific abilities for granular control.
Route::middleware(['auth:sanctum', 'ability:product:create,product:update'])->post('/products', [ProductController::class, 'store']);
if ($request->user()->tokenCan('product:delete')) {
// Allow delete
}
Controllers and Resources
Build RESTful controllers with authorization.
php artisan make:controller API/ProductController --api --model=Product
// app/Http/Controllers/API/ProductController.php
use App\Models\Product;
use App\Http\Resources\ProductResource;
use Illuminate\Http\Request;
public function index(Request $request)
{
$user = $request->user();
if (! $user->tokenCan('product:read')) {
return response()->json(['error' => 'Unauthorized'], 403);
}
$products = Product::where('created_by', $user->id)->paginate(10);
return ProductResource::collection($products);
}
public function store(Request $request)
{
$this->authorize('create', Product::class); // Using policies
$validated = $request->validate([
'name' => 'required|string|max:255',
'price' => 'required|numeric|min:0',
]);
$product = $request->user()->products()->create($validated);
return new ProductResource($product);
}
API Resources
Use resources for consistent JSON output.
php artisan make:resource ProductResource --collection
// app/Http/Resources/ProductResource.php
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'price' => $this->price,
'discounted_price' => $this->when($this->discount > 0, $this->price * (1 - $this->discount / 100)),
'created_at' => $this->created_at->format('Y-m-d'),
];
}
Authorization with Policies and Gates
Integrate with Laravel's authorization.
php artisan make:policy ProductPolicy --model=Product
// app/Policies/ProductPolicy.php
public function view(User $user, Product $product)
{
return $user->id === $product->user_id || $user->tokenCan('admin');
}
public function create(User $user)
{
return $user->tokenCan('product:create');
}
Register in AuthServiceProvider: $policies = [Product::class => ProductPolicy::class];.
In controller: $this->authorize('update', $product);.
Gates for Non-Model Actions
Gate::define('admin-access', function (User $user) {
return $user->is_admin;
});
Route::middleware(['auth:sanctum', 'can:admin-access'])->get('/admin', ...);
SPA Authentication
For stateful auth with cookies (CSRF-protected).
- Frontend (e.g., Vue) on stateful domain makes requests to Laravel backend.
- Login route sets session cookie.
- Subsequent API calls include cookie automatically.
// routes/api.php
Route::post('/login', [AuthController::class, 'login']); // No middleware
// AuthController
public function login(Request $request)
{
$credentials = $request->validate(['email' => 'required|email', 'password' => 'required']);
if (Auth::attempt($credentials)) {
$request->session()->regenerate();
return response()->json(['message' => 'Logged in']);
}
return response()->json(['error' => 'Invalid credentials'], 401);
}
Frontend: Use Axios with withCredentials: true. Sanctum handles CSRF via /sanctum/csrf-cookie endpoint.
// Vue.js example
axios.get('/sanctum/csrf-cookie').then(() => {
axios.post('/login', { email, password }).then(() => {
axios.get('/api/user').then(response => console.log(response.data));
});
});
Rate Limiting and Throttling
Prevent abuse on API endpoints.
// app/Http/Kernel.php
protected $routeMiddleware = [
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
];
Route::middleware(['auth:sanctum', 'throttle:60,1'])->post('/products', ...); // 60 requests per minute
Custom limits based on abilities or user roles.
Testing APIs
Use Laravel's testing tools with Sanctum.
public function test_create_product()
{
$user = User::factory()->create();
$token = $user->createToken('test', ['product:create'])->plainTextToken;
$response = $this->withHeaders(['Authorization' => "Bearer $token"])
->postJson('/api/products', ['name' => 'Test', 'price' => 10]);
$response->assertStatus(201);
$this->assertDatabaseHas('products', ['name' => 'Test']);
}
public function test_unauthorized_access()
{
$response = $this->getJson('/api/user');
$response->assertStatus(401);
}
Error Handling and Validation
Custom responses for API.
// app/Exceptions/Handler.php
public function render($request, Throwable $exception)
{
if ($request->expectsJson()) {
if ($exception instanceof AuthenticationException) {
return response()->json(['error' => 'Unauthenticated'], 401);
}
if ($exception instanceof AuthorizationException) {
return response()->json(['error' => 'Forbidden'], 403);
}
if ($exception instanceof ValidationException) {
return response()->json(['errors' => $exception->errors()], 422);
}
}
return parent::render($request, $exception);
}
Form requests for validation: php artisan make:request StoreProductRequest.
Advanced Features
- Token Revocation on Logout:
$request->user()->currentAccessToken()->delete(); - Multiple Guards: Combine with other auth systems.
- Pruning Old Tokens: Schedule
Sanctum::pruneExpired();in scheduler. - Custom Token Models: Extend for additional fields like IP binding.
Integration with Frontend Frameworks
For React: Use similar Axios setup. For mobile (Flutter/React Native): Use token-based auth and store tokens securely (e.g., Keychain).
Security Best Practices
- Use HTTPS in production.
- Limit abilities to least privilege.
- Rotate tokens periodically.
- Log auth attempts with events.
- Use Laravel Fortress or middleware for IP whitelisting.
- Audit with Laravel Telescope in dev.
- Never expose plain tokens in logs or frontend beyond necessity.
// Event listener for login
protected $listen = [
\Illuminate\Auth\Events\Login::class => [
\App\Listeners\LogSuccessfulLogin::class,
],
];
With Sanctum, you can build secure, scalable APIs quickly. For complex OAuth needs, consider Laravel Passport. Refer to Laravel 11.x docs for updates.