Skip to main content
Laravel, shipping fast.

CORS (Cross-Origin Resource Sharing) controls which domains can call your API. This prevents random websites from accessing your API without permission.

How CORS Works

When a browser makes a request to your API from a different domain, it first sends a "preflight" request with the OPTIONS method to ask: "Is this request allowed?" Only if your API responds with the right headers does the browser allow the actual request.

For example, a JavaScript app on https://client.com wants to POST to https://api.example.com/api/licenses. The browser:

  1. Sends: OPTIONS /api/licenses with header Origin: https://client.com
  2. Checks your response for Access-Control-Allow-Origin: https://client.com
  3. Only if permitted, sends the actual POST request

This prevents malicious websites from accessing your API on behalf of unwitting users.

Configure CORS to Allow Only Trusted Origins

// config/cors.php
return [
    'paths' => ['api/*'], // Apply to API routes only
    'allowed_methods' => ['*'], // GET, POST, DELETE, etc.
    'allowed_origins' => explode(',', env('CORS_ALLOWED_ORIGINS', 'http://localhost:3000')),
    'allowed_headers' => ['*'], // Accept any headers from clients
    'exposed_headers' => [
        'X-RateLimit-Limit',
        'X-RateLimit-Remaining',
        'X-RateLimit-Reset',
    ],
    'max_age' => 0, // Don't cache preflight (reevaluate every request)
    'supports_credentials' => false, // Don't allow cookies/credentials
];

// .env
CORS_ALLOWED_ORIGINS=http://localhost:3000,https://example.com

// Register in bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->api(append: [
        \Illuminate\Http\Middleware\HandleCors::class,
    ]);
})

Only the domains you specify can call your API. Everything else is rejected with a CORS error.

Important CORS Security Notes

  • allowed_origins must be explicit: Never allow * (all origins) if your API handles sensitive data. Specific origins only.
  • supports_credentials defaults to false: If you ever enable this to allow cookies, be extremely careful. It opens CSRF vulnerabilities. For Bearer token APIs, keep it false.
  • max_age controls preflight caching: We use 0 (no caching) for maximum security. Some APIs use higher values for performance, but then preflight policy changes take longer to propagate.
  • Client-side validation is not security: A malicious user can bypass CORS restrictions by using curl or Postman. CORS only stops browsers from accessing data. Never rely on CORS alone for security; always validate on the server.

HeaderFactory: Consistent Response Headers

Every response your API sends should include certain standard headers: security headers, caching directives, rate limit information. Rather than scattering header logic across your code, centralize it in HeaderFactory. This factory generates consistent headers for all response types.

// app/Factories/HeaderFactory.php
final readonly class HeaderFactory
{
    public static function default(): array
    {
        return [
            'Content-Type' => 'application/json',
            'X-Content-Type-Options' => 'nosniff',
            'X-Frame-Options' => 'DENY',
            'X-XSS-Protection' => '1; mode=block',
            'Referrer-Policy' => 'strict-origin-when-cross-origin',
            'Permissions-Policy' => 'geolocation=(), microphone=(), camera=()',
            'Strict-Transport-Security' => 'max-age=31536000; includeSubDomains',
        ];
    }

    public static function errors(): array
    {
        return array_merge(self::default(), [
            'Cache-Control' => 'no-cache, no-store, must-revalidate',
            'Pragma' => 'no-cache',
            'Expires' => '0',
        ]);
    }
}

Why centralize headers? Headers are security-critical. They tell the browser "don't sniff the content type," "don't embed me in a frame," "use HTTPS," and more. If you hardcode headers in multiple places, you risk inconsistency. Centralization means: every response gets the same security headers, changes propagate everywhere, testing is focused in one place, and new security headers are added without touching response wrappers.

When returning error responses, use HeaderFactory::errors() to add strict no-cache headers. Errors like 401 or 429 should never be cached by browsers or proxies.

Security headers like X-Content-Type-Options: nosniff prevent MIME type confusion attacks. X-Frame-Options: DENY prevents clickjacking. Strict-Transport-Security forces HTTPS for future requests.