add CORS manager with relative docs

This commit is contained in:
2026-07-16 15:18:24 +02:00
parent ec26db0bfa
commit 85843e99c7
6 changed files with 373 additions and 1 deletions
+8
View File
@@ -7,6 +7,7 @@ require_once __DIR__ . '/vendor/autoload.php';
use Api\Core\Container; use Api\Core\Container;
use Api\Core\Config; use Api\Core\Config;
use Api\Core\HttpClient; use Api\Core\HttpClient;
use Api\Core\CorsManager;
use Api\Core\RateLimiter\RateLimiterInterface; use Api\Core\RateLimiter\RateLimiterInterface;
use Api\Core\RateLimiter\FileRateLimiter; use Api\Core\RateLimiter\FileRateLimiter;
use Api\Core\RateLimiter\InMemoryRateLimiter; use Api\Core\RateLimiter\InMemoryRateLimiter;
@@ -25,6 +26,13 @@ $container->singleton(HttpClient::class, function() {
return new HttpClient(); return new HttpClient();
}); });
// CORS manager binding
$container->singleton(CorsManager::class, function($c) {
$config = $c->make(Config::class);
$corsConfig = $config->get('cors', []);
return new CorsManager($corsConfig);
});
// Rate limiter driver configurabile via config or env: 'file' or 'memory' // Rate limiter driver configurabile via config or env: 'file' or 'memory'
$rlDriver = $config->get('rate_limiter_driver', 'file'); $rlDriver = $config->get('rate_limiter_driver', 'file');
+39
View File
@@ -1,10 +1,49 @@
<?php <?php
return [ return [
'elixforms_api_base_url' => 'https://api.example.com', 'elixforms_api_base_url' => 'https://api.example.com',
'rate_limiter_driver' => 'file', // o 'memory' 'rate_limiter_driver' => 'file', // o 'memory'
// configuration for file-based rate limiter // configuration for file-based rate limiter
'rate_limit_storage_dir' => sys_get_temp_dir() . '/api_rate_limit', 'rate_limit_storage_dir' => sys_get_temp_dir() . '/api_rate_limit',
// configuration for memory-based rate limiter // configuration for memory-based rate limiter
'rate_limit_requests' => 100, 'rate_limit_requests' => 100,
'rate_limit_window_seconds' => 60, 'rate_limit_window_seconds' => 60,
'cors' => [
'enabled' => true,
// Allowed origins - requests from other origins will be rejected
'allowed_origins' => [
'http://localhost:3000', // Local development - frontend
'http://localhost:8080', // Local development - alternative port
'https://app.example.com', // Production frontend
'https://admin.example.com', // Production admin panel
// 'http://localhost:*', // Allow any port on localhost (not recommended)
// '*' // Allow all origins (HIGHLY NOT RECOMMENDED for production)
],
// HTTP methods allowed for CORS requests
'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
// HTTP headers allowed in the request
'allowed_headers' => [
'Content-Type',
'Authorization',
'X-Requested-With',
'Accept',
'Accept-Language',
'Content-Language',
'X-API-Key',
],
// HTTP headers exposed to the client
'exposed_headers' => [
'Content-Length',
'X-JSON-Response-Code',
'X-Rate-Limit-Limit',
'X-Rate-Limit-Remaining',
'X-Rate-Limit-Reset',
],
// Allow credentials (cookies, authorization headers) in cross-origin requests
// Only set to true if you understand the security implications
'allow_credentials' => false,
// How long (in seconds) the browser can cache the preflight response
'max_age' => 86400, // 24 hours
],
]; ];
+3 -1
View File
@@ -1,8 +1,10 @@
<?php <?php
return [ return [
// elixforms web services credentials
'api_access_token' => 'myApiAccessToken',
// elixforms API credentials // elixforms API credentials
'elixforms_api_username' => 'myUser', 'elixforms_api_username' => 'myUser',
'elixforms_api_password' => 'myPass', 'elixforms_api_password' => 'myPass',
'elixforms_api_token' => 'mySecretToken', 'elixforms_api_token' => 'mySecretToken',
'api_access_token' => 'myApiAccessToken',
]; ];
+168
View File
@@ -0,0 +1,168 @@
# CORS Management Documentation
## Overview
This API now includes comprehensive CORS (Cross-Origin Resource Sharing) management. CORS allows your API to be accessed from web applications hosted on different domains.
## How It Works
### Automatic CORS Header Application
All API responses automatically include CORS headers when CORS is enabled. This allows web applications from configured origins to access your API.
### Preflight Request Handling
The API automatically handles preflight OPTIONS requests (sent by browsers before actual requests to CORS-protected resources). These are handled with a 204 No Content response.
### Origin Validation
Only requests from configured allowed origins are accepted. Requests from unauthorized origins are rejected.
## Configuration
Edit `config/config.php` to configure CORS:
```php
'cors' => [
'enabled' => true, // Enable/disable CORS
'allowed_origins' => [
'http://localhost:3000',
'https://example.com',
],
'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
'allowed_headers' => ['Content-Type', 'Authorization', 'X-Requested-With', 'Accept'],
'exposed_headers' => ['Content-Length', 'X-JSON-Response-Code'],
'allow_credentials' => false,
'max_age' => 86400,
],
```
## Configuration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `enabled` | bool | `true` | Enable or disable CORS |
| `allowed_origins` | array | `[]` | List of domains allowed to access the API. Use `'*'` to allow all (NOT recommended) |
| `allowed_methods` | array | `GET, POST, PUT, DELETE, PATCH, OPTIONS` | HTTP methods allowed |
| `allowed_headers` | array | `Content-Type, Authorization, X-Requested-With, Accept` | Headers allowed in requests |
| `exposed_headers` | array | `Content-Length, X-JSON-Response-Code` | Headers exposed to the client |
| `allow_credentials` | bool | `false` | Allow credentials (cookies, auth) in requests |
| `max_age` | int | `86400` | Browser cache time for preflight (seconds) |
## Setup for Different Environments
### Development
```php
'cors' => [
'enabled' => true,
'allowed_origins' => [
'http://localhost:3000',
'http://localhost:8080',
'http://127.0.0.1:3000',
],
'allow_credentials' => false,
'max_age' => 3600,
],
```
### Production
```php
'cors' => [
'enabled' => true,
'allowed_origins' => [
'https://app.example.com',
'https://admin.example.com',
],
'allow_credentials' => false,
'max_age' => 86400,
],
```
### Allow All Origins (NOT RECOMMENDED for Production)
```php
'cors' => [
'enabled' => true,
'allowed_origins' => ['*'],
'allow_credentials' => false,
],
```
## Browser Preflight Requests
When making cross-origin requests with certain headers or methods (like PUT or DELETE), browsers automatically send a preflight OPTIONS request. The API handles these automatically:
```
Browser sends: OPTIONS /api/resource
API responds: 204 No Content + CORS headers
Browser sees: Request is allowed, proceeds with actual request
```
## Testing CORS
### Using curl
```bash
# Test CORS with curl
curl -H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type" \
-X OPTIONS \
http://localhost:8000/api/users/index -v
```
### Expected Response Headers
```
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With, Accept
Access-Control-Max-Age: 86400
```
## Security Considerations
1. **Be Specific with Origins**: Always specify exact allowed origins. Don't use `'*'` in production unless absolutely necessary.
2. **Credentials**: Only set `allow_credentials: true` if you understand the security implications. This allows cross-origin requests to send cookies.
3. **Sensitive Headers**: Don't expose sensitive headers in `exposed_headers`. Only expose what clients actually need.
4. **HTTPS in Production**: Always use HTTPS in production to prevent man-in-the-middle attacks.
## CorsManager Class
The `CorsManager` class handles all CORS logic. You can also use it programmatically:
```php
$corsManager = $container->make(\Api\Core\CorsManager::class);
// Check if current request is allowed
if ($corsManager->isOriginAllowed()) {
// Process request
}
// Apply CORS headers manually
$corsManager->applyHeaders();
// Check for preflight request
if ($corsManager->isPreflightRequest()) {
$corsManager->handlePreflight();
}
```
## Troubleshooting
### Preflight request fails
- Check that the origin in the request matches one in `allowed_origins`
- Verify CORS is enabled in config
- Check browser console for CORS error messages
### Missing CORS headers in response
- Ensure CORS is enabled: `'enabled' => true`
- Verify the requesting origin is in `allowed_origins`
- Check that `CorsManager` is properly initialized in the container
### "No Access-Control-Allow-Origin header"
- Browser origin is not in `allowed_origins`
- Add the origin or use `'*'` (only for development)
## References
- [MDN: CORS Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
- [OWASP: CORS](https://owasp.org/www-community/CORS)
+43
View File
@@ -5,6 +5,7 @@ use Api\Auth\ApiTokenAuthenticator;
use Api\Core\Router; use Api\Core\Router;
use Api\Core\Request; use Api\Core\Request;
use Api\Core\Response; use Api\Core\Response;
use Api\Core\CorsManager;
use Api\Core\RateLimiter\RateLimiterInterface; use Api\Core\RateLimiter\RateLimiterInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@@ -32,6 +33,48 @@ $router = new Router($container); // vedi nota: router può ricevere container
$request = new Request(); $request = new Request();
$response = new Response(); $response = new Response();
// CORS handling
$corsManager = $container->make(CorsManager::class);
$corsConfig = $container->make(\Api\Core\Config::class)->get('cors', []);
if (!empty($corsConfig['enabled'])) {
$origin = $corsManager->getOrigin();
$requestOrigin = $_SERVER['HTTP_ORIGIN'] ?? 'none';
// Log CORS request details for debugging
$logger->debug('CORS request received', [
'request_origin' => $requestOrigin,
'allowed_origin' => $origin,
'is_preflight' => $corsManager->isPreflightRequest(),
'method' => $_SERVER['REQUEST_METHOD'],
'path' => $request->path()
]);
// Apply CORS headers to all responses
$corsManager->applyHeaders();
// Handle preflight OPTIONS requests
if ($corsManager->isPreflightRequest()) {
$corsManager->handlePreflight();
}
}
// Diagnostic CORS endpoint (no auth required)
if ($request->path() === '/cors-check' && $request->method() === 'GET') {
$corsManager = $container->make(CorsManager::class);
$corsConfig = $container->make(\Api\Core\Config::class)->get('cors', []);
$response->json([
'cors_enabled' => !empty($corsConfig['enabled']),
'request_origin' => $_SERVER['HTTP_ORIGIN'] ?? null,
'allowed_origins' => $corsConfig['allowed_origins'] ?? [],
'is_origin_allowed' => $corsManager->isOriginAllowed(),
'is_preflight' => $corsManager->isPreflightRequest(),
'request_method' => $_SERVER['REQUEST_METHOD'],
'headers_sent' => function_exists('getallheaders') ? getallheaders() : $_SERVER,
]);
}
// Autenticazione separata per le API esterne // Autenticazione separata per le API esterne
if (strpos($request->path(), '/api/') === 0) { if (strpos($request->path(), '/api/') === 0) {
try { try {
+112
View File
@@ -0,0 +1,112 @@
<?php
namespace Api\Core;
class CorsManager {
private $allowedOrigins;
private $allowedMethods;
private $allowedHeaders;
private $exposedHeaders;
private $allowCredentials;
private $maxAge;
public function __construct(array $config = []) {
$this->allowedOrigins = $config['allowed_origins'] ?? [];
$this->allowedMethods = $config['allowed_methods'] ?? ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'];
$this->allowedHeaders = $config['allowed_headers'] ?? ['Content-Type', 'Authorization', 'X-Requested-With'];
$this->exposedHeaders = $config['exposed_headers'] ?? ['Content-Length', 'X-JSON-Response-Code'];
$this->allowCredentials = $config['allow_credentials'] ?? false;
$this->maxAge = $config['max_age'] ?? 86400;
}
/**
* Get the allowed origin for the current request
*/
public function getOrigin(): ?string {
$origin = $_SERVER['HTTP_ORIGIN'] ?? null;
if (!$origin) {
return null;
}
// Check if origin is in allowed list
if (in_array('*', $this->allowedOrigins)) {
return '*';
}
if (in_array($origin, $this->allowedOrigins)) {
return $origin;
}
return null;
}
/**
* Check if the current request is a preflight OPTIONS request
*/
public function isPreflightRequest(): bool {
return $_SERVER['REQUEST_METHOD'] === 'OPTIONS';
}
/**
* Apply CORS headers to the response
*/
public function applyHeaders(?string $origin = null): void {
if ($origin === null) {
$origin = $this->getOrigin();
}
if (!$origin) {
// If no valid origin, don't set CORS headers
// This ensures blocked origins don't accidentally get access
header('Vary: Origin');
return;
}
header('Access-Control-Allow-Origin: ' . $origin);
header('Access-Control-Allow-Methods: ' . implode(', ', $this->allowedMethods));
header('Access-Control-Allow-Headers: ' . implode(', ', $this->allowedHeaders));
header('Access-Control-Expose-Headers: ' . implode(', ', $this->exposedHeaders));
header('Vary: Origin');
if ($this->allowCredentials) {
header('Access-Control-Allow-Credentials: true');
}
header('Access-Control-Max-Age: ' . $this->maxAge);
}
/**
* Handle preflight OPTIONS request
*/
public function handlePreflight(): void {
$origin = $this->getOrigin();
if ($origin) {
$this->applyHeaders($origin);
http_response_code(204);
exit;
}
http_response_code(403);
exit;
}
/**
* Check if origin is allowed
*/
public function isOriginAllowed(?string $origin = null): bool {
if ($origin === null) {
$origin = $this->getOrigin();
}
if (!$origin) {
return false;
}
if (in_array('*', $this->allowedOrigins)) {
return true;
}
return in_array($origin, $this->allowedOrigins);
}
}