Most Laravel API tutorials cover Sanctum or Passport and call it done. Authentication is table stakes — it’s the minimum. A production API serving real business data needs significantly more.
This guide covers advanced Laravel API security for 2026 — the layer above authentication that most developers skip.
The OWASP API Security Top 10 — 2026 Relevance
OWASP’s API Security Top 10 is the industry standard for API vulnerabilities. Here’s how each maps to Laravel:
| Vulnerability | Laravel Risk | Fix |
|---|---|---|
| Broken Object Level Authorization | High | Policies + scoped queries |
| Broken Authentication | Medium (Sanctum helps) | Token rotation, 2FA |
| Broken Object Property Level Auth | High | API Resources, never $fillable * |
| Unrestricted Resource Consumption | High | Rate limiting, pagination |
| Broken Function Level Authorization | Medium | Gates and Policies |
| Unrestricted Access to Business Flows | Medium | Business logic validation |
| Server Side Request Forgery | Low-Medium | Validate URLs, block internal ranges |
| Security Misconfiguration | High | Config hardening (covered below) |
| Improper Inventory Management | Medium | API versioning, deprecation |
| Unsafe Consumption of APIs | Medium | Validate third-party responses |
1. Advanced Rate Limiting
Laravel’s default throttle middleware is a start, but production APIs need more granular control:
// routes/api.php — granular rate limits per route group
// Strict limits for auth endpoints (prevent brute force)
Route::middleware('throttle:5,1')->group(function () {
Route::post('/login', [AuthController::class, 'login']);
Route::post('/register', [AuthController::class, 'register']);
Route::post('/forgot-password', [AuthController::class, 'forgotPassword']);
});
// Standard API limits
Route::middleware(['auth:sanctum', 'throttle:60,1'])->group(function () {
Route::apiResource('tasks', TaskController::class);
});
// Higher limits for trusted internal services
Route::middleware(['auth:sanctum', 'throttle:1000,1'])->group(function () {
Route::get('/internal/sync', [SyncController::class, 'index']);
});
For more sophisticated rate limiting, use named limiters:
// app/Providers/RouteServiceProvider.php
protected function configureRateLimiting(): void
{
// Per-user rate limit
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
// Stricter limit for expensive operations
RateLimiter::for('reports', function (Request $request) {
return [
Limit::perMinute(5)->by($request->user()->id),
Limit::perDay(50)->by($request->user()->id),
];
});
// IP-based limit for public endpoints
RateLimiter::for('public', function (Request $request) {
return Limit::perMinute(10)->by($request->ip());
});
}
2. Object-Level Authorization (IDOR Prevention)
The most common API vulnerability — accessing another user’s data by changing an ID in the URL:
// ❌ VULNERABLE — anyone can access any task by ID
public function show(Task $task): TaskResource
{
return new TaskResource($task);
}
// ✅ SECURE — scope query to authenticated user
public function show(Request $request, int $id): TaskResource
{
$task = $request->user()->tasks()->findOrFail($id);
return new TaskResource($task);
}
// ✅ ALSO SECURE — use Laravel Policies
public function show(Request $request, Task $task): TaskResource
{
$this->authorize('view', $task); // TaskPolicy checks ownership
return new TaskResource($task);
}
Create a policy:
php artisan make:policy TaskPolicy --model=Task
// app/Policies/TaskPolicy.php
public function view(User $user, Task $task): bool
{
return $user->id === $task->user_id;
}
public function update(User $user, Task $task): bool
{
return $user->id === $task->user_id;
}
public function delete(User $user, Task $task): bool
{
return $user->id === $task->user_id;
}
3. API Response Security — Never Expose Internal Data
// ❌ DANGEROUS — exposes internal fields
public function show(User $user)
{
return response()->json($user); // Exposes password hash, remember_token, etc.
}
// ✅ SECURE — API Resource controls exactly what's exposed
class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at->toDateString(),
// Never: password, remember_token, email_verified_at (internal)
];
}
}
4. Request Signing for Webhooks
When your API sends webhooks to client systems, sign the payload so they can verify it came from you:
// Sending a signed webhook
class WebhookService
{
public function send(string $url, array $payload, string $secret): void
{
$json = json_encode($payload);
$timestamp = time();
$signature = hash_hmac('sha256', $timestamp . '.' . $json, $secret);
Http::withHeaders([
'X-Webhook-Timestamp' => $timestamp,
'X-Webhook-Signature' => $signature,
'Content-Type' => 'application/json',
])->post($url, $payload);
}
}
// Verifying an incoming webhook
class IncomingWebhookController extends Controller
{
public function handle(Request $request): JsonResponse
{
$timestamp = $request->header('X-Webhook-Timestamp');
$signature = $request->header('X-Webhook-Signature');
$secret = config('services.webhook_secret');
// Reject old webhooks (replay attack prevention)
if (abs(time() - $timestamp) > 300) {
abort(401, 'Webhook timestamp too old');
}
$expected = hash_hmac('sha256', $timestamp . '.' . $request->getContent(), $secret);
if (!hash_equals($expected, $signature)) {
abort(401, 'Invalid webhook signature');
}
// Process the webhook
WebhookReceived::dispatch($request->all());
return response()->json(['status' => 'received']);
}
}
5. CORS Hardening
// config/cors.php — production hardening
return [
'paths' => ['api/*'],
'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
// Never use '*' in production
'allowed_origins' => [
'https://app.yourdomain.com',
'https://yourdomain.com',
],
'allowed_origins_patterns' => [],
'allowed_headers' => ['Content-Type', 'Authorization', 'X-Requested-With'],
'exposed_headers' => [],
'max_age' => 86400,
'supports_credentials' => true,
];
6. Security Headers
Add a middleware to inject security headers on all API responses:
php artisan make:middleware SecurityHeaders
// app/Http/Middleware/SecurityHeaders.php
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('X-XSS-Protection', '1; mode=block');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$response->headers->set('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
// Remove fingerprinting headers
$response->headers->remove('X-Powered-By');
$response->headers->remove('Server');
return $response;
}
Register in bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->api(prepend: [
\App\Http\Middleware\SecurityHeaders::class,
]);
})
7. Sanctum Token Security
// Set token expiry — tokens shouldn't live forever
$token = $user->createToken(
name: 'api-token',
abilities: ['read', 'write'],
expiresAt: now()->addDays(30) // Expire after 30 days
);
// Scope tokens to specific abilities
$readOnlyToken = $user->createToken('read-only', ['read']);
$adminToken = $user->createToken('admin', ['read', 'write', 'delete']);
// Check ability in controller
public function destroy(Request $request, Task $task): JsonResponse
{
if (!$request->user()->tokenCan('delete')) {
abort(403, 'This token cannot delete resources');
}
$task->delete();
return response()->json(['message' => 'Deleted']);
}
Security Audit Checklist
| Check | Status |
|---|---|
| All routes behind authentication | ☐ |
| Rate limiting on all endpoints | ☐ |
| Stricter limits on auth endpoints | ☐ |
| Object-level authorization on all resources | ☐ |
| API Resources used — no raw model returns | ☐ |
| CORS locked to specific origins | ☐ |
| Security headers middleware active | ☐ |
| Token expiry configured | ☐ |
| Token abilities scoped | ☐ |
| Webhook signatures verified | ☐ |
| Sensitive fields excluded from responses | ☐ |
| APP_DEBUG=false in production | ☐ |
If you need a security audit of your existing Laravel API or want to build a new API with security built in from day one, our backend team at Softcrony can help.
Leave a comment