CORS Configuration Recipe#
Set up Cross-Origin Resource Sharing for your API.
Problem#
Your API needs to be accessible from web applications hosted on different domains.
Solution#
Basic CORS Middleware#
<?php
// src/Middleware/CorsMiddleware.php
namespace App\Middleware;
use Infocyph\Webrick\Request\Request;
use Infocyph\Webrick\Response\Response;
use Closure;
final class CorsMiddleware
{
private array $allowedOrigins;
private array $allowedMethods;
private array $allowedHeaders;
private int $maxAge;
private bool $allowCredentials;
public function __construct(
array $allowedOrigins = ['*'],
array $allowedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
array $allowedHeaders = ['Content-Type', 'Authorization'],
int $maxAge = 3600,
bool $allowCredentials = false
) {
$this->allowedOrigins = $allowedOrigins;
$this->allowedMethods = $allowedMethods;
$this->allowedHeaders = $allowedHeaders;
$this->maxAge = $maxAge;
$this->allowCredentials = $allowCredentials;
}
public function __invoke(Request $r, Closure $next): Response
{
$origin = $r->getHeaderLine('Origin');
// Handle preflight request
if ($r->isOptions()) {
return $this->handlePreflight($origin);
}
// Handle actual request
$response = $next($r);
return $this->addCorsHeaders($response, $origin);
}
private function handlePreflight(string $origin): Response
{
$response = Response::create('', 204);
return $this->addCorsHeaders($response, $origin)
->withHeader('Access-Control-Max-Age', (string) $this->maxAge);
}
private function addCorsHeaders(Response $response, string $origin): Response
{
// Check if origin is allowed
if (!$this->isOriginAllowed($origin)) {
return $response;
}
$response = $response
->withHeader('Access-Control-Allow-Origin', $this->getAllowedOrigin($origin))
->withHeader('Access-Control-Allow-Methods', implode(', ', $this->allowedMethods))
->withHeader('Access-Control-Allow-Headers', implode(', ', $this->allowedHeaders));
if ($this->allowCredentials) {
$response = $response->withHeader('Access-Control-Allow-Credentials', 'true');
}
return $response;
}
private function isOriginAllowed(string $origin): bool
{
if (in_array('*', $this->allowedOrigins)) {
return true;
}
return in_array($origin, $this->allowedOrigins);
}
private function getAllowedOrigin(string $origin): string
{
if (in_array('*', $this->allowedOrigins)) {
return $this->allowCredentials ? $origin : '*';
}
return $origin;
}
}
Configuration#
<?php
// config/cors.php
return [
// Development
'development' => [
'allowed_origins' => ['*'],
'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
'allowed_headers' => ['*'],
'max_age' => 3600,
'allow_credentials' => false
],
// Production
'production' => [
'allowed_origins' => [
'https://app.example.com',
'https://admin.example.com'
],
'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
'allowed_headers' => ['Content-Type', 'Authorization', 'X-Requested-With'],
'max_age' => 86400, // 24 hours
'allow_credentials' => true
]
];
// Usage
$corsConfig = require __DIR__ . '/../config/cors.php';
$env = $_ENV['APP_ENV'] ?? 'development';
$config = $corsConfig[$env];
$preGlobal[] = new CorsMiddleware(
allowedOrigins: $config['allowed_origins'],
allowedMethods: $config['allowed_methods'],
allowedHeaders: $config['allowed_headers'],
maxAge: $config['max_age'],
allowCredentials: $config['allow_credentials']
);
Testing#
# Test preflight request
curl -X OPTIONS http://localhost:8000/api/users \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type" \
-v
# Expected headers:
# Access-Control-Allow-Origin: https://app.example.com
# Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
# Access-Control-Allow-Headers: Content-Type, Authorization
# Access-Control-Max-Age: 3600
# Test actual request
curl http://localhost:8000/api/users \
-H "Origin: https://app.example.com" \
-v
Best Practices#
1. Never Use * with Credentials#
// ❌ Bad: Security risk
new CorsMiddleware(
allowedOrigins: ['*'],
allowCredentials: true
);
// ✅ Good: Explicit origins
new CorsMiddleware(
allowedOrigins: ['https://app.example.com'],
allowCredentials: true
);
2. Whitelist Specific Origins#
// ❌ Bad: Too permissive
$allowedOrigins = ['*'];
// ✅ Good: Explicit whitelist
$allowedOrigins = [
'https://app.example.com',
'https://admin.example.com',
'https://mobile.example.com'
];
3. Limit Allowed Headers#
// ❌ Bad: All headers allowed
$allowedHeaders = ['*'];
// ✅ Good: Only necessary headers
$allowedHeaders = [
'Content-Type',
'Authorization',
'X-Requested-With',
'X-API-Key'
];
4. Set Appropriate Max-Age#
// Development: Short cache
$maxAge = 600; // 10 minutes
// Production: Longer cache
$maxAge = 86400; // 24 hours
Summary#
This recipe provides:
✅ Complete CORS middleware
✅ Preflight request handling
✅ Environment-based configuration
✅ Origin validation
✅ Credentials support
Production checklist:
Whitelist specific origins (never use
*with credentials)Limit allowed methods
Limit allowed headers
Set appropriate max-age
Test preflight and actual requests
Monitor CORS errors