|
| 1 | +# MCP HTTP Security |
| 2 | + |
| 3 | +Secure HTTP transport wrapper for MCP (Model Context Protocol) servers in PHP. |
| 4 | + |
| 5 | +Provides production-ready security components that don't exist elsewhere in the PHP MCP ecosystem: |
| 6 | + |
| 7 | +- **API Key Authentication** - Secure key generation, hashing (SHA-256 + pepper), TTL/expiry |
| 8 | +- **IP Allowlisting** - CIDR notation, IPv4/IPv6 support |
| 9 | +- **Origin Allowlisting** - Hostname validation with wildcard subdomain support |
| 10 | +- **PSR-15 Middleware** - Drop-in security for any PSR-15 compatible framework |
| 11 | + |
| 12 | +## Installation |
| 13 | + |
| 14 | +```bash |
| 15 | +composer require code-wheel/mcp-http-security |
| 16 | +``` |
| 17 | + |
| 18 | +## Quick Start |
| 19 | + |
| 20 | +```php |
| 21 | +<?php |
| 22 | + |
| 23 | +use CodeWheel\McpSecurity\ApiKey\ApiKeyManager; |
| 24 | +use CodeWheel\McpSecurity\ApiKey\Storage\FileStorage; |
| 25 | +use CodeWheel\McpSecurity\Clock\SystemClock; |
| 26 | +use CodeWheel\McpSecurity\Config\SecurityConfig; |
| 27 | +use CodeWheel\McpSecurity\Middleware\SecurityMiddleware; |
| 28 | +use CodeWheel\McpSecurity\Validation\RequestValidator; |
| 29 | + |
| 30 | +// 1. Setup API Key Manager |
| 31 | +$storage = new FileStorage('/var/data/mcp-api-keys.json'); |
| 32 | +$clock = new SystemClock(); |
| 33 | +$apiKeyManager = new ApiKeyManager( |
| 34 | + storage: $storage, |
| 35 | + clock: $clock, |
| 36 | + pepper: getenv('MCP_API_KEY_PEPPER') ?: '', |
| 37 | +); |
| 38 | + |
| 39 | +// 2. Create a key |
| 40 | +$result = $apiKeyManager->createKey( |
| 41 | + label: 'Claude Code', |
| 42 | + scopes: ['read', 'write'], |
| 43 | + ttlSeconds: 86400 * 30, // 30 days |
| 44 | +); |
| 45 | +echo "API Key: {$result['api_key']}\n"; // Show once, store securely |
| 46 | + |
| 47 | +// 3. Setup Request Validator |
| 48 | +$validator = new RequestValidator( |
| 49 | + allowedIps: ['127.0.0.1', '10.0.0.0/8'], |
| 50 | + allowedOrigins: ['localhost', '*.example.com'], |
| 51 | +); |
| 52 | + |
| 53 | +// 4. Create Middleware |
| 54 | +$middleware = new SecurityMiddleware( |
| 55 | + apiKeyManager: $apiKeyManager, |
| 56 | + requestValidator: $validator, |
| 57 | + responseFactory: new HttpFactory(), // Any PSR-17 factory |
| 58 | + config: new SecurityConfig( |
| 59 | + requireAuth: true, |
| 60 | + allowedScopes: ['read', 'write'], |
| 61 | + ), |
| 62 | +); |
| 63 | + |
| 64 | +// 5. Use with your PSR-15 application |
| 65 | +$app->pipe($middleware); |
| 66 | +``` |
| 67 | + |
| 68 | +## API Key Management |
| 69 | + |
| 70 | +### Creating Keys |
| 71 | + |
| 72 | +```php |
| 73 | +$result = $apiKeyManager->createKey( |
| 74 | + label: 'Production API', |
| 75 | + scopes: ['read', 'write', 'admin'], |
| 76 | + ttlSeconds: null, // No expiry |
| 77 | +); |
| 78 | + |
| 79 | +// Returns: |
| 80 | +// [ |
| 81 | +// 'key_id' => 'abc123def456', |
| 82 | +// 'api_key' => 'mcp.abc123def456.secret...', |
| 83 | +// ] |
| 84 | +``` |
| 85 | + |
| 86 | +### Listing Keys |
| 87 | + |
| 88 | +```php |
| 89 | +$keys = $apiKeyManager->listKeys(); |
| 90 | + |
| 91 | +foreach ($keys as $keyId => $key) { |
| 92 | + echo "{$key->label} - Scopes: " . implode(', ', $key->scopes) . "\n"; |
| 93 | + echo " Created: " . date('Y-m-d', $key->created) . "\n"; |
| 94 | + echo " Expires: " . ($key->expires ? date('Y-m-d', $key->expires) : 'Never') . "\n"; |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +### Validating Keys |
| 99 | + |
| 100 | +```php |
| 101 | +$apiKey = $apiKeyManager->validate($tokenFromRequest); |
| 102 | + |
| 103 | +if ($apiKey === null) { |
| 104 | + // Invalid or expired |
| 105 | +} |
| 106 | + |
| 107 | +if ($apiKey->hasScope('write')) { |
| 108 | + // Allow write operation |
| 109 | +} |
| 110 | +``` |
| 111 | + |
| 112 | +### Revoking Keys |
| 113 | + |
| 114 | +```php |
| 115 | +$apiKeyManager->revokeKey('abc123def456'); |
| 116 | +``` |
| 117 | + |
| 118 | +## Storage Backends |
| 119 | + |
| 120 | +### File Storage (Simple) |
| 121 | + |
| 122 | +```php |
| 123 | +use CodeWheel\McpSecurity\ApiKey\Storage\FileStorage; |
| 124 | + |
| 125 | +$storage = new FileStorage('/var/data/api-keys.json'); |
| 126 | +``` |
| 127 | + |
| 128 | +### Database Storage (PDO) |
| 129 | + |
| 130 | +```php |
| 131 | +use CodeWheel\McpSecurity\ApiKey\Storage\PdoStorage; |
| 132 | + |
| 133 | +$pdo = new PDO('mysql:host=localhost;dbname=app', 'user', 'pass'); |
| 134 | +$storage = new PdoStorage($pdo, 'mcp_api_keys'); |
| 135 | +$storage->ensureTable(); // Creates table if needed |
| 136 | +``` |
| 137 | + |
| 138 | +### In-Memory (Testing) |
| 139 | + |
| 140 | +```php |
| 141 | +use CodeWheel\McpSecurity\ApiKey\Storage\ArrayStorage; |
| 142 | + |
| 143 | +$storage = new ArrayStorage(); |
| 144 | +``` |
| 145 | + |
| 146 | +### Custom Storage |
| 147 | + |
| 148 | +Implement `StorageInterface`: |
| 149 | + |
| 150 | +```php |
| 151 | +use CodeWheel\McpSecurity\ApiKey\Storage\StorageInterface; |
| 152 | + |
| 153 | +class RedisStorage implements StorageInterface |
| 154 | +{ |
| 155 | + public function getAll(): array { /* ... */ } |
| 156 | + public function setAll(array $keys): void { /* ... */ } |
| 157 | + public function get(string $keyId): ?array { /* ... */ } |
| 158 | + public function set(string $keyId, array $data): void { /* ... */ } |
| 159 | + public function delete(string $keyId): bool { /* ... */ } |
| 160 | +} |
| 161 | +``` |
| 162 | + |
| 163 | +## Request Validation |
| 164 | + |
| 165 | +### IP Allowlisting |
| 166 | + |
| 167 | +```php |
| 168 | +use CodeWheel\McpSecurity\Validation\IpValidator; |
| 169 | + |
| 170 | +$validator = new IpValidator([ |
| 171 | + '127.0.0.1', // Single IP |
| 172 | + '10.0.0.0/8', // CIDR range |
| 173 | + '192.168.0.0/16', // Private network |
| 174 | + '::1', // IPv6 localhost |
| 175 | +]); |
| 176 | + |
| 177 | +$validator->isAllowed('10.5.3.2'); // true |
| 178 | +$validator->isAllowed('8.8.8.8'); // false |
| 179 | +``` |
| 180 | + |
| 181 | +### Origin Allowlisting |
| 182 | + |
| 183 | +```php |
| 184 | +use CodeWheel\McpSecurity\Validation\OriginValidator; |
| 185 | + |
| 186 | +$validator = new OriginValidator([ |
| 187 | + 'localhost', |
| 188 | + 'example.com', |
| 189 | + '*.example.com', // Wildcard: foo.example.com, bar.example.com |
| 190 | +]); |
| 191 | + |
| 192 | +$validator->isAllowed('api.example.com'); // true |
| 193 | +$validator->isAllowed('evil.com'); // false |
| 194 | +``` |
| 195 | + |
| 196 | +### Combined Request Validation |
| 197 | + |
| 198 | +```php |
| 199 | +use CodeWheel\McpSecurity\Validation\RequestValidator; |
| 200 | + |
| 201 | +$validator = new RequestValidator( |
| 202 | + allowedIps: ['127.0.0.1', '10.0.0.0/8'], |
| 203 | + allowedOrigins: ['localhost', '*.myapp.com'], |
| 204 | +); |
| 205 | + |
| 206 | +// With PSR-7 request |
| 207 | +$validator->validate($request); // Throws ValidationException if invalid |
| 208 | +$validator->isValid($request); // Returns bool |
| 209 | +``` |
| 210 | + |
| 211 | +## Middleware Configuration |
| 212 | + |
| 213 | +```php |
| 214 | +use CodeWheel\McpSecurity\Config\SecurityConfig; |
| 215 | + |
| 216 | +$config = new SecurityConfig( |
| 217 | + requireAuth: true, // Require API key |
| 218 | + allowedScopes: ['read'], // Only allow these scopes |
| 219 | + authHeader: 'Authorization', // Bearer token header |
| 220 | + apiKeyHeader: 'X-MCP-Api-Key', // Alternative header |
| 221 | + scopesAttribute: 'mcp.scopes', // Request attribute for scopes |
| 222 | + keyAttribute: 'mcp.key', // Request attribute for key info |
| 223 | + silentFail: true, // Return 404 instead of 401/403 |
| 224 | +); |
| 225 | +``` |
| 226 | + |
| 227 | +## Error Handling |
| 228 | + |
| 229 | +The middleware throws typed exceptions: |
| 230 | + |
| 231 | +```php |
| 232 | +use CodeWheel\McpSecurity\Exception\AuthenticationException; |
| 233 | +use CodeWheel\McpSecurity\Exception\AuthorizationException; |
| 234 | +use CodeWheel\McpSecurity\Exception\RateLimitException; |
| 235 | +use CodeWheel\McpSecurity\Exception\ValidationException; |
| 236 | + |
| 237 | +try { |
| 238 | + $middleware->process($request, $handler); |
| 239 | +} catch (AuthenticationException $e) { |
| 240 | + // 401 - Invalid or missing API key |
| 241 | +} catch (AuthorizationException $e) { |
| 242 | + // 403 - Insufficient scopes |
| 243 | + echo "Required: " . implode(', ', $e->requiredScopes); |
| 244 | + echo "Actual: " . implode(', ', $e->actualScopes); |
| 245 | +} catch (ValidationException $e) { |
| 246 | + // 404 - IP/Origin not allowed |
| 247 | +} catch (RateLimitException $e) { |
| 248 | + // 429 - Rate limited |
| 249 | + echo "Retry after: {$e->retryAfterSeconds} seconds"; |
| 250 | +} |
| 251 | +``` |
| 252 | + |
| 253 | +## Framework Integration |
| 254 | + |
| 255 | +### Slim 4 |
| 256 | + |
| 257 | +```php |
| 258 | +$app->add($securityMiddleware); |
| 259 | +``` |
| 260 | + |
| 261 | +### Laravel |
| 262 | + |
| 263 | +```php |
| 264 | +// In a service provider |
| 265 | +$this->app->singleton(SecurityMiddleware::class, function ($app) { |
| 266 | + return new SecurityMiddleware(/* ... */); |
| 267 | +}); |
| 268 | + |
| 269 | +// In Kernel.php |
| 270 | +protected $middleware = [ |
| 271 | + \CodeWheel\McpSecurity\Middleware\SecurityMiddleware::class, |
| 272 | +]; |
| 273 | +``` |
| 274 | + |
| 275 | +### Drupal |
| 276 | + |
| 277 | +See [drupal/mcp_tools](https://www.drupal.org/project/mcp_tools) which uses this package. |
| 278 | + |
| 279 | +## Security Considerations |
| 280 | + |
| 281 | +1. **Pepper your hashes** - Always provide a pepper for API key hashing |
| 282 | +2. **Use HTTPS** - Never transmit API keys over unencrypted connections |
| 283 | +3. **Rotate keys** - Use TTL and rotate keys regularly |
| 284 | +4. **Least privilege** - Grant minimal scopes needed |
| 285 | +5. **Audit logging** - Log key usage for security monitoring |
| 286 | + |
| 287 | +## License |
| 288 | + |
| 289 | +MIT License - see [LICENSE](LICENSE) file. |
| 290 | + |
| 291 | +## Credits |
| 292 | + |
| 293 | +Extracted from [drupal/mcp_tools](https://www.drupal.org/project/mcp_tools) by [CodeWheel](https://github.com/code-wheel). |
0 commit comments