add CORS manager with relative docs
This commit is contained in:
@@ -7,6 +7,7 @@ require_once __DIR__ . '/vendor/autoload.php';
|
||||
use Api\Core\Container;
|
||||
use Api\Core\Config;
|
||||
use Api\Core\HttpClient;
|
||||
use Api\Core\CorsManager;
|
||||
use Api\Core\RateLimiter\RateLimiterInterface;
|
||||
use Api\Core\RateLimiter\FileRateLimiter;
|
||||
use Api\Core\RateLimiter\InMemoryRateLimiter;
|
||||
@@ -25,6 +26,13 @@ $container->singleton(HttpClient::class, function() {
|
||||
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'
|
||||
$rlDriver = $config->get('rate_limiter_driver', 'file');
|
||||
|
||||
|
||||
@@ -1,10 +1,49 @@
|
||||
<?php
|
||||
return [
|
||||
'elixforms_api_base_url' => 'https://api.example.com',
|
||||
|
||||
'rate_limiter_driver' => 'file', // o 'memory'
|
||||
// configuration for file-based rate limiter
|
||||
'rate_limit_storage_dir' => sys_get_temp_dir() . '/api_rate_limit',
|
||||
// configuration for memory-based rate limiter
|
||||
'rate_limit_requests' => 100,
|
||||
'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
|
||||
],
|
||||
];
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<?php
|
||||
return [
|
||||
// elixforms web services credentials
|
||||
'api_access_token' => 'myApiAccessToken',
|
||||
|
||||
// elixforms API credentials
|
||||
'elixforms_api_username' => 'myUser',
|
||||
'elixforms_api_password' => 'myPass',
|
||||
'elixforms_api_token' => 'mySecretToken',
|
||||
'api_access_token' => 'myApiAccessToken',
|
||||
];
|
||||
|
||||
+168
@@ -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)
|
||||
@@ -5,6 +5,7 @@ use Api\Auth\ApiTokenAuthenticator;
|
||||
use Api\Core\Router;
|
||||
use Api\Core\Request;
|
||||
use Api\Core\Response;
|
||||
use Api\Core\CorsManager;
|
||||
use Api\Core\RateLimiter\RateLimiterInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
@@ -32,6 +33,48 @@ $router = new Router($container); // vedi nota: router può ricevere container
|
||||
$request = new Request();
|
||||
$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
|
||||
if (strpos($request->path(), '/api/') === 0) {
|
||||
try {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user