| name | api-security |
| description | API security patterns for Laravel. Rate limiting, headers, CORS, Sanctum tokens, input validation. Use when building or securing API endpoints. |
API Security
Secure your APIs against abuse, injection, and unauthorized access.
When to Use
- Building REST APIs
- Configuring authentication with Sanctum
- Setting up rate limiting
- Configuring CORS
- Securing webhooks
1. Authentication with Sanctum
Token-Based (API clients)
$token = $user->createToken('api-token', ['read', 'write'])->plainTextToken;
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', fn () => auth()->user());
Route::apiResource('posts', PostController::class);
});
if ($user->tokenCan('write')) {
}
SPA Authentication (Cookie-based)
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1'
)),
'supports_credentials' => true,
await fetch('/sanctum/csrf-cookie');
await fetch('/login', { method: 'POST', credentials: 'include' });
Token Best Practices
$token = $user->createToken('payment', ['payments'])->plainTextToken;
$user->tokens()->where('last_used_at', '<', now()->subDays(30))->delete();
$user->tokens()->delete();
2. Rate Limiting
Define Limiters
protected function configureRateLimiting(): void
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)
->by($request->user()?->id ?: $request->ip());
});
RateLimiter::for('auth', function (Request $request) {
return Limit::perMinute(5)->by($request->ip());
});
RateLimiter::for('tiered', function (Request $request) {
$user = $request->user();
return match ($user?->plan) {
'premium' => Limit::perMinute(1000)->by($user->id),
'pro' => Limit::perMinute(100)->by($user->id),
default => Limit::perMinute(20)->by($request->ip()),
};
});
}
Apply to Routes
Route::middleware(['throttle:auth'])->group(function () {
Route::post('/login', [AuthController::class, 'login']);
Route::post('/register', [AuthController::class, 'register']);
});
Route::middleware(['auth:sanctum', 'throttle:tiered'])->group(function () {
Route::apiResource('posts', PostController::class);
});
Rate Limit Headers
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
Retry-After: 60
3. CORS Configuration
config/cors.php
return [
'allowed_origins' => [
'https://app.example.com',
'https://admin.example.com',
],
'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
'allowed_headers' => [
'Content-Type',
'Authorization',
'X-Requested-With',
'Accept',
],
'exposed_headers' => [
'X-RateLimit-Limit',
'X-RateLimit-Remaining',
],
'supports_credentials' => true,
'max_age' => 86400,
];
Environment-Based Config
'allowed_origins' => explode(',', env('CORS_ALLOWED_ORIGINS', '')),
4. Security Headers
Middleware
class ApiSecurityHeaders
{
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', '0');
$response->headers->set('Cache-Control', 'no-store');
$response->headers->set('Pragma', 'no-cache');
return $response;
}
}
Register in Kernel
protected $middlewareGroups = [
'api' => [
\App\Http\Middleware\ApiSecurityHeaders::class,
],
];
5. Input Validation
API Form Requests
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('create', Post::class);
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string', 'max:65535'],
'status' => ['required', Rule::enum(PostStatus::class)],
];
}
}
Consistent Error Responses
protected function invalidJson($request, ValidationException $exception): JsonResponse
{
return response()->json([
'message' => 'Validation failed',
'errors' => $exception->errors(),
], 422);
}
6. Response Security
Never Expose Internals
return response()->json([
'user' => $user, // May include sensitive fields
'debug' => $exception->getTrace(),
]);
return response()->json([
'user' => new UserResource($user),
]);
API Resources
class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
];
}
}
7. Webhook Security
Verify Signatures
class StripeWebhookController extends Controller
{
public function handle(Request $request)
{
$signature = $request->header('Stripe-Signature');
$payload = $request->getContent();
$secret = config('services.stripe.webhook_secret');
try {
$event = \Stripe\Webhook::constructEvent(
$payload,
$signature,
$secret
);
} catch (\Exception $e) {
abort(400, 'Invalid signature');
}
}
}
Webhook Best Practices
8. API Versioning
Header-Based (Preferred)
public function handle(Request $request, Closure $next)
{
$version = $request->header('Accept-Version', 'v1');
$request->attributes->set('api_version', $version);
return $next($request);
}
URL-Based
Route::prefix('v1')->group(function () {
Route::apiResource('posts', V1\PostController::class);
});
Route::prefix('v2')->group(function () {
Route::apiResource('posts', V2\PostController::class);
});
Quick Checklist
Before Deploying API
Remember: APIs are public attack surfaces. Every endpoint is a potential entry point.