- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
1074 lines
29 KiB
Markdown
1074 lines
29 KiB
Markdown
# LiveComponents Security Guide
|
|
|
|
Comprehensive security guide for implementing and using LiveComponents with CSRF protection, rate limiting, and idempotency.
|
|
|
|
## Table of Contents
|
|
|
|
- [Overview](#overview)
|
|
- [CSRF Protection](#csrf-protection)
|
|
- [Rate Limiting](#rate-limiting)
|
|
- [Idempotency Keys](#idempotency-keys)
|
|
- [State Encryption](#state-encryption)
|
|
- [Combined Security](#combined-security)
|
|
- [Best Practices](#best-practices)
|
|
- [Testing Security](#testing-security)
|
|
- [Troubleshooting](#troubleshooting)
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
LiveComponents implements a **defense-in-depth security model** with four core layers:
|
|
|
|
1. **CSRF Protection** - Prevents cross-site request forgery attacks
|
|
2. **Rate Limiting** - Protects against abuse and DDoS attacks
|
|
3. **Idempotency Keys** - Prevents duplicate operations from network issues
|
|
4. **State Encryption** - Protects sensitive state data with authenticated encryption
|
|
|
|
All security features are **enabled by default** and require no configuration for basic usage.
|
|
|
|
### Security Processing Order
|
|
|
|
```
|
|
Request → CSRF Validation → Rate Limit Check → Idempotency Check → Action Execution
|
|
```
|
|
|
|
If any layer fails, the request is rejected **before** reaching your component logic.
|
|
|
|
---
|
|
|
|
## CSRF Protection
|
|
|
|
### How It Works
|
|
|
|
Every LiveComponent action requires a valid CSRF token to execute. Tokens are:
|
|
- **Per-session**: One token per user session
|
|
- **Auto-rotated**: New token generated after each action
|
|
- **Short-lived**: Expires with the session
|
|
|
|
### Automatic Token Injection
|
|
|
|
The framework automatically injects CSRF tokens into your component templates:
|
|
|
|
```html
|
|
<!-- Your template -->
|
|
<form method="POST" data-lc-action="submitForm">
|
|
<input type="text" name="email" />
|
|
<button type="submit">Submit</button>
|
|
</form>
|
|
|
|
<!-- Automatically becomes -->
|
|
<form method="POST" data-lc-action="submitForm">
|
|
<input type="hidden" name="_csrf_token" value="auto_generated_token" />
|
|
<input type="text" name="email" />
|
|
<button type="submit">Submit</button>
|
|
</form>
|
|
```
|
|
|
|
### JavaScript Integration
|
|
|
|
The LiveComponent JavaScript client automatically includes CSRF tokens:
|
|
|
|
```javascript
|
|
// Automatic - no configuration needed
|
|
liveComponent.executeAction('increment');
|
|
|
|
// The client automatically adds:
|
|
// { _csrf_token: 'session_token' }
|
|
```
|
|
|
|
### Manual Token Access
|
|
|
|
For custom forms or AJAX requests:
|
|
|
|
```php
|
|
// In your controller or component
|
|
$csrfToken = $this->session->getCsrfToken();
|
|
|
|
// In JavaScript
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
|
|
|
|
fetch('/api/livecomponents/action', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
body: JSON.stringify({
|
|
component_id: 'counter:main',
|
|
action: 'increment'
|
|
})
|
|
});
|
|
```
|
|
|
|
### CSRF Exceptions
|
|
|
|
```php
|
|
use App\Framework\Exception\Security\CsrfTokenMismatchException;
|
|
|
|
try {
|
|
$this->handler->handleAction($component, 'increment', $params);
|
|
} catch (CsrfTokenMismatchException $e) {
|
|
// Token is invalid, expired, or missing
|
|
// User should refresh the page
|
|
|
|
// Log security event
|
|
$this->owaspLogger->logSecurityEvent(
|
|
OWASPEventIdentifier::CSRF_ATTACK_DETECTED,
|
|
$request
|
|
);
|
|
|
|
return new JsonResponse([
|
|
'error' => 'Invalid security token. Please refresh the page.',
|
|
'code' => 'CSRF_TOKEN_INVALID'
|
|
], Status::FORBIDDEN);
|
|
}
|
|
```
|
|
|
|
### Testing CSRF Protection
|
|
|
|
```php
|
|
use function Pest\LiveComponents\mountComponent;
|
|
use function Pest\LiveComponents\callAction;
|
|
|
|
it('requires valid CSRF token for actions', function () {
|
|
$component = mountComponent('counter:test', ['count' => 0]);
|
|
|
|
// Without CSRF token should fail
|
|
expect(fn() => callAction($component, 'increment', [
|
|
'_csrf_token' => 'invalid_token'
|
|
]))->toThrow(CsrfTokenMismatchException::class);
|
|
});
|
|
|
|
it('accepts valid CSRF token', function () {
|
|
$component = mountComponent('counter:test', ['count' => 0]);
|
|
$session = container()->get(Session::class);
|
|
$csrfToken = $session->getCsrfToken();
|
|
|
|
$result = callAction($component, 'increment', [
|
|
'_csrf_token' => $csrfToken
|
|
]);
|
|
|
|
expect($result['state']['count'])->toBe(1);
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Rate Limiting
|
|
|
|
### How It Works
|
|
|
|
Rate limiting prevents abuse by restricting the number of actions per time window:
|
|
|
|
- **Per-Client**: Tracked by IP address + User ID (if authenticated)
|
|
- **Per-Action**: Each action can have custom limits
|
|
- **Configurable**: Set via `#[Action]` attribute
|
|
- **Sliding Window**: Uses token bucket algorithm
|
|
|
|
### Default Limits
|
|
|
|
```php
|
|
// Framework defaults (can be overridden)
|
|
const DEFAULT_RATE_LIMIT = 60; // requests
|
|
const DEFAULT_WINDOW = 60; // seconds
|
|
```
|
|
|
|
### Custom Rate Limits per Action
|
|
|
|
```php
|
|
use App\Framework\LiveComponents\Attributes\Action;
|
|
use App\Framework\Core\ValueObjects\Duration;
|
|
|
|
#[LiveComponent('payment')]
|
|
final readonly class PaymentComponent implements LiveComponentContract
|
|
{
|
|
// Strict limit for payment processing
|
|
#[Action(rateLimit: 5, rateLimitWindow: 300)] // 5 per 5 minutes
|
|
public function processPayment(PaymentRequest $request): ComponentData
|
|
{
|
|
// Process payment
|
|
}
|
|
|
|
// More lenient for viewing
|
|
#[Action(rateLimit: 100, rateLimitWindow: 60)] // 100 per minute
|
|
public function viewTransactions(): ComponentData
|
|
{
|
|
// View transactions
|
|
}
|
|
|
|
// No custom limit - uses framework default (60 per minute)
|
|
#[Action]
|
|
public function updateProfile(ProfileData $data): ComponentData
|
|
{
|
|
// Update profile
|
|
}
|
|
}
|
|
```
|
|
|
|
### Rate Limit Responses
|
|
|
|
When rate limit is exceeded:
|
|
|
|
```json
|
|
{
|
|
"error": "Rate limit exceeded",
|
|
"code": "RATE_LIMIT_EXCEEDED",
|
|
"retry_after": 45,
|
|
"limit": 60,
|
|
"window": 60
|
|
}
|
|
```
|
|
|
|
HTTP Status: **429 Too Many Requests**
|
|
|
|
Headers:
|
|
```
|
|
X-RateLimit-Limit: 60
|
|
X-RateLimit-Remaining: 0
|
|
X-RateLimit-Reset: 1234567890
|
|
Retry-After: 45
|
|
```
|
|
|
|
### Handling Rate Limits in JavaScript
|
|
|
|
```javascript
|
|
liveComponent.on('error', (error) => {
|
|
if (error.code === 'RATE_LIMIT_EXCEEDED') {
|
|
const retryAfter = error.retry_after;
|
|
|
|
// Show user-friendly message
|
|
showNotification(
|
|
`Too many requests. Please wait ${retryAfter} seconds.`,
|
|
'warning'
|
|
);
|
|
|
|
// Optionally schedule retry
|
|
setTimeout(() => {
|
|
liveComponent.executeAction('retry');
|
|
}, retryAfter * 1000);
|
|
}
|
|
});
|
|
```
|
|
|
|
### Rate Limit Exceptions
|
|
|
|
```php
|
|
use App\Framework\Exception\Http\RateLimitExceededException;
|
|
|
|
try {
|
|
$this->handler->handleAction($component, 'processPayment', $params);
|
|
} catch (RateLimitExceededException $e) {
|
|
$retryAfter = $e->getRetryAfter(); // seconds
|
|
|
|
// Log for monitoring
|
|
$this->logger->warning('Rate limit exceeded', [
|
|
'component' => $component->id->toString(),
|
|
'action' => 'processPayment',
|
|
'retry_after' => $retryAfter,
|
|
'client_ip' => $request->server->getClientIp()
|
|
]);
|
|
|
|
return new JsonResponse([
|
|
'error' => 'Too many payment attempts',
|
|
'retry_after' => $retryAfter
|
|
], Status::TOO_MANY_REQUESTS);
|
|
}
|
|
```
|
|
|
|
### Testing Rate Limiting
|
|
|
|
```php
|
|
it('enforces rate limits on actions', function () {
|
|
$component = mountComponent('counter:test', ['count' => 0]);
|
|
|
|
// Execute action multiple times rapidly
|
|
for ($i = 0; $i < 10; $i++) {
|
|
callAction($component, 'increment');
|
|
}
|
|
|
|
// 11th request should be rate limited
|
|
expect(fn() => callAction($component, 'increment'))
|
|
->toThrow(RateLimitExceededException::class);
|
|
});
|
|
|
|
it('includes retry-after in rate limit response', function () {
|
|
$component = mountComponent('counter:test', ['count' => 0]);
|
|
|
|
// Exhaust rate limit
|
|
for ($i = 0; $i < 10; $i++) {
|
|
callAction($component, 'increment');
|
|
}
|
|
|
|
try {
|
|
callAction($component, 'increment');
|
|
} catch (RateLimitExceededException $e) {
|
|
expect($e->getRetryAfter())->toBeGreaterThan(0);
|
|
}
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Idempotency Keys
|
|
|
|
### How It Works
|
|
|
|
Idempotency keys prevent duplicate operations from:
|
|
- Network timeouts
|
|
- Double-clicks
|
|
- Browser back/forward
|
|
- Client-side retries
|
|
|
|
When the same idempotency key is used twice:
|
|
- First request: **Executes action**, caches result
|
|
- Subsequent requests: **Returns cached result**, no execution
|
|
|
|
### Automatic Idempotency
|
|
|
|
The LiveComponent JavaScript client automatically generates idempotency keys for critical actions:
|
|
|
|
```javascript
|
|
// Framework automatically adds idempotency key for state-changing actions
|
|
liveComponent.executeAction('processPayment', {
|
|
amount: 100
|
|
});
|
|
|
|
// Internally becomes:
|
|
{
|
|
action: 'processPayment',
|
|
params: { amount: 100 },
|
|
idempotency_key: 'auto_generated_uuid_v4'
|
|
}
|
|
```
|
|
|
|
### Manual Idempotency Keys
|
|
|
|
For custom control or server-side operations:
|
|
|
|
```php
|
|
use App\Framework\LiveComponents\Attributes\Action;
|
|
|
|
#[LiveComponent('order')]
|
|
final readonly class OrderComponent implements LiveComponentContract
|
|
{
|
|
#[Action(idempotencyTTL: 3600)] // Cache for 1 hour
|
|
public function createOrder(
|
|
OrderRequest $request,
|
|
ActionParameters $params
|
|
): ComponentData {
|
|
// Idempotency key is automatically extracted and validated
|
|
// If duplicate key: returns cached result without executing
|
|
|
|
$order = $this->orderService->create($request);
|
|
|
|
return $this->state->withOrder($order);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Custom Idempotency Key Generation
|
|
|
|
```javascript
|
|
// Generate consistent key for retry scenarios
|
|
const idempotencyKey = `order-${orderId}-${Date.now()}`;
|
|
|
|
liveComponent.executeAction('createOrder', {
|
|
order_id: orderId,
|
|
items: cartItems
|
|
}, {
|
|
idempotency_key: idempotencyKey
|
|
});
|
|
|
|
// Retry with same key - will return cached result
|
|
setTimeout(() => {
|
|
liveComponent.executeAction('createOrder', {
|
|
order_id: orderId,
|
|
items: cartItems
|
|
}, {
|
|
idempotency_key: idempotencyKey // Same key = cached result
|
|
});
|
|
}, 5000);
|
|
```
|
|
|
|
### Idempotency Metadata
|
|
|
|
Response includes metadata about idempotency:
|
|
|
|
```json
|
|
{
|
|
"html": "<div>Order created</div>",
|
|
"state": { "order_id": "12345" },
|
|
"idempotency": {
|
|
"key": "order-12345-1234567890",
|
|
"cached": false,
|
|
"ttl": 3600,
|
|
"created_at": "2024-01-15T10:30:00Z"
|
|
}
|
|
}
|
|
```
|
|
|
|
On duplicate request:
|
|
```json
|
|
{
|
|
"html": "<div>Order created</div>",
|
|
"state": { "order_id": "12345" },
|
|
"idempotency": {
|
|
"key": "order-12345-1234567890",
|
|
"cached": true, // ← Indicates cached result
|
|
"ttl": 3540,
|
|
"created_at": "2024-01-15T10:30:00Z"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Idempotency TTL
|
|
|
|
Configure cache duration per action:
|
|
|
|
```php
|
|
// Short TTL for volatile operations
|
|
#[Action(idempotencyTTL: 300)] // 5 minutes
|
|
public function updateCart(): ComponentData
|
|
|
|
// Long TTL for critical operations
|
|
#[Action(idempotencyTTL: 86400)] // 24 hours
|
|
public function processPayment(): ComponentData
|
|
|
|
// No idempotency (default: 3600 seconds / 1 hour)
|
|
#[Action]
|
|
public function viewProfile(): ComponentData
|
|
```
|
|
|
|
### Testing Idempotency
|
|
|
|
```php
|
|
it('prevents duplicate action execution with same idempotency key', function () {
|
|
$component = mountComponent('counter:test', ['count' => 0]);
|
|
$idempotencyKey = 'test-key-' . uniqid();
|
|
|
|
// First execution
|
|
$result1 = callAction($component, 'increment', [
|
|
'idempotency_key' => $idempotencyKey
|
|
]);
|
|
|
|
expect($result1['state']['count'])->toBe(1);
|
|
|
|
// Second execution with same key should return cached result
|
|
$result2 = callAction($result1, 'increment', [
|
|
'idempotency_key' => $idempotencyKey
|
|
]);
|
|
|
|
// Count should still be 1 (not 2)
|
|
expect($result2['state']['count'])->toBe(1);
|
|
expect($result2['idempotency']['cached'])->toBeTrue();
|
|
});
|
|
|
|
it('allows different actions with different idempotency keys', function () {
|
|
$component = mountComponent('counter:test', ['count' => 0]);
|
|
|
|
$result1 = callAction($component, 'increment', [
|
|
'idempotency_key' => 'key-1'
|
|
]);
|
|
|
|
$result2 = callAction($result1, 'increment', [
|
|
'idempotency_key' => 'key-2'
|
|
]);
|
|
|
|
expect($result2['state']['count'])->toBe(2);
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## State Encryption
|
|
|
|
### Overview
|
|
|
|
Sensitive LiveComponent state data is automatically encrypted using authenticated encryption (XSalsa20-Poly1305 via libsodium) to protect against:
|
|
- State tampering
|
|
- Unauthorized access to sensitive data
|
|
- Replay attacks with modified state
|
|
- Side-channel attacks
|
|
|
|
### When to Use State Encryption
|
|
|
|
Encrypt state when it contains:
|
|
- **Sensitive User Data**: Email addresses, phone numbers, personal information
|
|
- **Session Tokens**: Authentication tokens, API keys, session identifiers
|
|
- **Financial Data**: Payment information, account balances, transaction details
|
|
- **Private Business Data**: Internal IDs, unpublished content, proprietary information
|
|
|
|
### Automatic Encryption via Transformer
|
|
|
|
```php
|
|
use App\Framework\StateManagement\CacheBasedStateManager;
|
|
use App\Framework\StateManagement\EncryptionTransformer;
|
|
use App\Framework\LiveComponents\Serialization\EncryptedStateSerializer;
|
|
use App\Framework\LiveComponents\Serialization\StateEncryptor;
|
|
|
|
// 1. Setup encryption components
|
|
$encryptionKey = $vault->get('state_encryption_key'); // 32-byte key from Vault
|
|
$encryptor = new StateEncryptor($encryptionKey, $crypto, $random);
|
|
$serializer = new EncryptedStateSerializer($encryptor);
|
|
|
|
// 2. Add EncryptionTransformer to StateManager pipeline
|
|
$encryptionTransformer = new EncryptionTransformer($serializer);
|
|
|
|
$stateManager = new CacheBasedStateManager(
|
|
$cache,
|
|
transformers: [$encryptionTransformer] // Automatic encryption/decryption
|
|
);
|
|
|
|
// 3. Use normally - encryption happens transparently
|
|
$stateManager->store($componentId, $userState);
|
|
$state = $stateManager->retrieve($componentId, UserState::class);
|
|
```
|
|
|
|
### Creating Encryptable State
|
|
|
|
```php
|
|
use App\Framework\StateManagement\SerializableState;
|
|
|
|
final readonly class UserProfileState implements SerializableState
|
|
{
|
|
public function __construct(
|
|
public string $userId,
|
|
public Email $email,
|
|
public string $sessionToken, // Sensitive - will be encrypted
|
|
public array $preferences // Sensitive - will be encrypted
|
|
) {}
|
|
|
|
public function toArray(): array
|
|
{
|
|
return [
|
|
'user_id' => $this->userId,
|
|
'email' => $this->email->value,
|
|
'session_token' => $this->sessionToken,
|
|
'preferences' => $this->preferences,
|
|
];
|
|
}
|
|
|
|
public static function fromArray(array $data): self
|
|
{
|
|
return new self(
|
|
userId: $data['user_id'],
|
|
email: new Email($data['email']),
|
|
sessionToken: $data['session_token'],
|
|
preferences: $data['preferences']
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Security Features
|
|
|
|
**Authenticated Encryption**:
|
|
- **Algorithm**: XSalsa20-Poly1305 (via `sodium_crypto_secretbox`)
|
|
- **Key Size**: 256-bit (32 bytes)
|
|
- **Nonce**: 24 bytes, unique per encryption operation
|
|
- **MAC**: Poly1305 authentication tag automatically included
|
|
- **Tampering Detection**: MAC verification on every decryption
|
|
|
|
**Encrypted Data Format**:
|
|
```
|
|
[Version:1][Nonce:24 bytes][Ciphertext + MAC]
|
|
└─ Base64 encoded for cache storage
|
|
```
|
|
|
|
### Error Handling
|
|
|
|
```php
|
|
use App\Framework\LiveComponents\Exceptions\StateEncryptionException;
|
|
|
|
try {
|
|
$state = $stateManager->retrieve($componentId, UserState::class);
|
|
} catch (StateEncryptionException $e) {
|
|
// Encryption/decryption failed
|
|
// Possible causes:
|
|
// - Wrong encryption key
|
|
// - Corrupted encrypted data
|
|
// - MAC verification failed (tampering detected)
|
|
|
|
// SECURITY: Never expose encryption details to client
|
|
error_log("State decryption failed: " . $e->getMessage());
|
|
|
|
// Regenerate state from source or show error
|
|
throw new \RuntimeException('Session invalid');
|
|
}
|
|
```
|
|
|
|
### Key Management
|
|
|
|
**Generating Encryption Keys**:
|
|
```php
|
|
use App\Framework\Random\SecureRandomGenerator;
|
|
|
|
$random = new SecureRandomGenerator();
|
|
$encryptionKey = $random->bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES); // 32 bytes
|
|
|
|
// Store in Vault (encrypted at rest)
|
|
$vault->store('state_encryption_key', $encryptionKey);
|
|
```
|
|
|
|
**Key Storage Best Practices**:
|
|
- ✅ Store in Vault (encrypted at rest)
|
|
- ✅ Separate keys per environment (dev/staging/prod)
|
|
- ✅ Rotate keys quarterly
|
|
- ❌ DO NOT commit keys to version control
|
|
- ❌ DO NOT store in .env files
|
|
- ❌ DO NOT store in plain text database
|
|
|
|
### Performance
|
|
|
|
- **Encryption Overhead**: ~1-2ms per operation
|
|
- **Recommended**: Use for sensitive data only
|
|
- **Caching**: Encrypted states cached normally (encryption is transparent)
|
|
- **TTL**: Consider shorter TTL for encrypted states (balance security/performance)
|
|
|
|
### Testing Encrypted State
|
|
|
|
```php
|
|
it('encrypts and decrypts user state correctly', function () {
|
|
$userState = new UserProfileState(
|
|
userId: 'user123',
|
|
email: new Email('test@example.com'),
|
|
sessionToken: 'secret-token-xyz',
|
|
preferences: ['theme' => 'dark']
|
|
);
|
|
|
|
// Store with encryption
|
|
$stateManager->store($componentId, $userState);
|
|
|
|
// Retrieve and decrypt
|
|
$retrieved = $stateManager->retrieve($componentId, UserProfileState::class);
|
|
|
|
expect($retrieved->sessionToken)->toBe('secret-token-xyz');
|
|
expect($retrieved->preferences)->toBe(['theme' => 'dark']);
|
|
});
|
|
|
|
it('detects state tampering via MAC verification', function () {
|
|
$stateManager->store($componentId, $userState);
|
|
|
|
// Tamper with encrypted state in cache
|
|
$encrypted = $cache->get($componentId);
|
|
$tampered = substr($encrypted, 0, -5) . 'XXXXX';
|
|
$cache->set($componentId, $tampered);
|
|
|
|
// Should throw StateEncryptionException
|
|
$stateManager->retrieve($componentId, UserProfileState::class);
|
|
})->throws(StateEncryptionException::class, 'MAC verification failed');
|
|
```
|
|
|
|
### Migration to Encrypted State
|
|
|
|
```php
|
|
// Step 1: Add EncryptionTransformer to existing StateManager
|
|
$stateManager = new CacheBasedStateManager(
|
|
$cache,
|
|
transformers: [
|
|
$encryptionTransformer // Add encryption transformer
|
|
]
|
|
);
|
|
|
|
// Step 2: Existing unencrypted states will be:
|
|
// - Read normally (transformer detects unencrypted format)
|
|
// - Re-encrypted on next update
|
|
|
|
// Step 3: Monitor for encryption errors during transition
|
|
// Old states may fail decryption - regenerate from source
|
|
```
|
|
|
|
**See Also**:
|
|
- [Security Patterns - State Encryption](/docs/claude/security-patterns.md#state-encryption-for-livecomponents)
|
|
- [StateEncryptor API Reference](../Serialization/StateEncryptor.php)
|
|
- [EncryptionTransformer API Reference](../../StateManagement/EncryptionTransformer.php)
|
|
|
|
---
|
|
|
|
## Combined Security
|
|
|
|
All four security layers work together:
|
|
|
|
### Validation Order
|
|
|
|
1. **CSRF Token** - Validates authenticity
|
|
2. **Rate Limit** - Checks request frequency
|
|
3. **Idempotency Key** - Prevents duplicates
|
|
4. **Action Execution** - Runs your logic
|
|
5. **State Encryption** - Encrypts sensitive state before storage
|
|
|
|
```php
|
|
// Example: Payment processing with all security layers
|
|
#[LiveComponent('payment')]
|
|
final readonly class PaymentComponent implements LiveComponentContract
|
|
{
|
|
// Sensitive state with encryption
|
|
public function __construct(
|
|
private StateManager $stateManager // Configured with EncryptionTransformer
|
|
) {}
|
|
|
|
#[Action(
|
|
rateLimit: 5, // Max 5 payment attempts
|
|
rateLimitWindow: 300, // Per 5 minutes
|
|
idempotencyTTL: 86400 // Cache result for 24 hours
|
|
)]
|
|
public function processPayment(
|
|
PaymentRequest $request,
|
|
ActionParameters $params
|
|
): ComponentData {
|
|
// Security Layers Applied (in order):
|
|
// 1. CSRF: Already validated by framework
|
|
// 2. Rate Limit: Already checked by framework
|
|
// 3. Idempotency: Cached result returned if duplicate key
|
|
|
|
// 4. Action Execution: Only if all security checks pass
|
|
$payment = $this->paymentGateway->charge($request);
|
|
|
|
// 5. State Encryption: Sensitive payment data automatically encrypted
|
|
return $this->state->withPayment($payment);
|
|
// ↑ StateManager encrypts this state before cache storage
|
|
}
|
|
}
|
|
```
|
|
|
|
### Security Event Flow
|
|
|
|
```
|
|
1. Client sends request with CSRF token + idempotency key
|
|
2. Framework validates CSRF token
|
|
├─ Invalid → 403 Forbidden (CsrfTokenMismatchException)
|
|
└─ Valid → Continue
|
|
3. Framework checks rate limit
|
|
├─ Exceeded → 429 Too Many Requests (RateLimitExceededException)
|
|
└─ OK → Continue
|
|
4. Framework checks idempotency key
|
|
├─ Duplicate → Return cached result (no execution)
|
|
└─ New → Continue
|
|
5. Execute action and cache result
|
|
6. Return response + new CSRF token
|
|
```
|
|
|
|
### Testing Combined Security
|
|
|
|
```php
|
|
it('enforces all security layers together', function () {
|
|
$component = mountComponent('payment:test', ['balance' => 1000]);
|
|
$session = container()->get(Session::class);
|
|
$csrfToken = $session->getCsrfToken();
|
|
$idempotencyKey = 'payment-' . uniqid();
|
|
|
|
// Valid request with all security features
|
|
$result = callAction($component, 'processPayment', [
|
|
'_csrf_token' => $csrfToken,
|
|
'idempotency_key' => $idempotencyKey,
|
|
'amount' => 100
|
|
]);
|
|
|
|
expect($result['state']['balance'])->toBe(900);
|
|
|
|
// Retry with same idempotency key but new CSRF token
|
|
$newCsrfToken = $session->getCsrfToken();
|
|
|
|
$result2 = callAction($result, 'processPayment', [
|
|
'_csrf_token' => $newCsrfToken,
|
|
'idempotency_key' => $idempotencyKey,
|
|
'amount' => 100
|
|
]);
|
|
|
|
// Should return cached result due to idempotency
|
|
expect($result2['state']['balance'])->toBe(900); // Still 900, not 800
|
|
expect($result2['idempotency']['cached'])->toBeTrue();
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Best Practices
|
|
|
|
### 1. Always Use Idempotency for Critical Operations
|
|
|
|
```php
|
|
// ✅ Good - Payments are idempotent
|
|
#[Action(idempotencyTTL: 86400)]
|
|
public function processPayment(PaymentRequest $request): ComponentData
|
|
|
|
// ✅ Good - Order creation is idempotent
|
|
#[Action(idempotencyTTL: 3600)]
|
|
public function createOrder(OrderRequest $request): ComponentData
|
|
|
|
// ⚠️ Warning - View operations don't need idempotency
|
|
#[Action] // No idempotency needed for reads
|
|
public function viewProfile(): ComponentData
|
|
```
|
|
|
|
### 2. Set Appropriate Rate Limits
|
|
|
|
```php
|
|
// ✅ Good - Strict limits for critical actions
|
|
#[Action(rateLimit: 5, rateLimitWindow: 300)] // 5 per 5 min
|
|
public function processPayment(): ComponentData
|
|
|
|
// ✅ Good - Lenient for viewing
|
|
#[Action(rateLimit: 100, rateLimitWindow: 60)] // 100 per min
|
|
public function viewDashboard(): ComponentData
|
|
|
|
// ❌ Bad - Too lenient for critical action
|
|
#[Action(rateLimit: 1000, rateLimitWindow: 60)]
|
|
public function deleteAccount(): ComponentData
|
|
```
|
|
|
|
### 3. Handle Security Exceptions Gracefully
|
|
|
|
```php
|
|
try {
|
|
$result = $this->handler->handleAction($component, $action, $params);
|
|
} catch (CsrfTokenMismatchException $e) {
|
|
// User-friendly message
|
|
return new JsonResponse([
|
|
'error' => 'Your session has expired. Please refresh the page.',
|
|
'code' => 'SESSION_EXPIRED',
|
|
'action' => 'REFRESH_PAGE'
|
|
], Status::FORBIDDEN);
|
|
} catch (RateLimitExceededException $e) {
|
|
// Include retry information
|
|
return new JsonResponse([
|
|
'error' => 'Too many requests. Please try again later.',
|
|
'code' => 'RATE_LIMIT_EXCEEDED',
|
|
'retry_after' => $e->getRetryAfter()
|
|
], Status::TOO_MANY_REQUESTS);
|
|
}
|
|
```
|
|
|
|
### 4. Log Security Events
|
|
|
|
```php
|
|
use App\Framework\Security\OWASPSecurityLogger;
|
|
use App\Framework\Security\OWASPEventIdentifier;
|
|
|
|
// Log failed CSRF attempts
|
|
$this->owaspLogger->logSecurityEvent(
|
|
OWASPEventIdentifier::CSRF_ATTACK_DETECTED,
|
|
$request,
|
|
context: [
|
|
'component' => $componentId,
|
|
'action' => $actionName,
|
|
'expected_token' => substr($expectedToken, 0, 8) . '...',
|
|
'received_token' => substr($receivedToken, 0, 8) . '...'
|
|
]
|
|
);
|
|
|
|
// Log rate limit violations
|
|
$this->owaspLogger->logSecurityEvent(
|
|
OWASPEventIdentifier::RATE_LIMIT_EXCEEDED,
|
|
$request,
|
|
context: [
|
|
'component' => $componentId,
|
|
'action' => $actionName,
|
|
'limit' => $limit,
|
|
'window' => $window,
|
|
'retry_after' => $retryAfter
|
|
]
|
|
);
|
|
```
|
|
|
|
### 5. Test All Security Layers
|
|
|
|
```php
|
|
// Test suite should cover:
|
|
it('validates CSRF tokens', function () { /* ... */ });
|
|
it('enforces rate limits', function () { /* ... */ });
|
|
it('prevents duplicate operations', function () { /* ... */ });
|
|
it('combines all security layers', function () { /* ... */ });
|
|
it('handles security exceptions gracefully', function () { /* ... */ });
|
|
```
|
|
|
|
---
|
|
|
|
## Testing Security
|
|
|
|
Complete test examples in `tests/Feature/LiveComponents/SecurityTest.php`:
|
|
|
|
```php
|
|
describe('LiveComponent Security', function () {
|
|
describe('CSRF Protection', function () {
|
|
// ... CSRF tests
|
|
});
|
|
|
|
describe('Rate Limiting', function () {
|
|
// ... Rate limit tests
|
|
});
|
|
|
|
describe('Idempotency Keys', function () {
|
|
// ... Idempotency tests
|
|
});
|
|
|
|
describe('Combined Security Features', function () {
|
|
// ... Integration tests
|
|
});
|
|
});
|
|
```
|
|
|
|
Run security tests:
|
|
```bash
|
|
./vendor/bin/pest tests/Feature/LiveComponents/SecurityTest.php
|
|
```
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### CSRF Token Issues
|
|
|
|
**Problem**: "CSRF token mismatch" errors
|
|
|
|
**Causes**:
|
|
- Session expired
|
|
- Multiple browser tabs
|
|
- Cookie issues (SameSite, Secure)
|
|
- Cache/CDN stripping tokens
|
|
|
|
**Solutions**:
|
|
```php
|
|
// 1. Check session configuration
|
|
$sessionConfig = [
|
|
'cookie_lifetime' => 3600,
|
|
'cookie_secure' => true, // HTTPS only
|
|
'cookie_samesite' => 'Strict', // Prevent CSRF
|
|
'cookie_httponly' => true // No JavaScript access
|
|
];
|
|
|
|
// 2. Add token refresh mechanism
|
|
public function refreshCsrfToken(): JsonResponse
|
|
{
|
|
$newToken = $this->session->regenerateCsrfToken();
|
|
|
|
return new JsonResponse([
|
|
'csrf_token' => $newToken
|
|
]);
|
|
}
|
|
|
|
// 3. Client-side token refresh
|
|
liveComponent.on('csrf_error', async () => {
|
|
const response = await fetch('/api/csrf/refresh');
|
|
const { csrf_token } = await response.json();
|
|
|
|
// Retry original request with new token
|
|
liveComponent.setCsrfToken(csrf_token);
|
|
});
|
|
```
|
|
|
|
### Rate Limit Issues
|
|
|
|
**Problem**: Legitimate users hitting rate limits
|
|
|
|
**Causes**:
|
|
- Shared IP addresses (NAT, VPN)
|
|
- Aggressive polling/refreshing
|
|
- Automated testing without cleanup
|
|
- Incorrect rate limit configuration
|
|
|
|
**Solutions**:
|
|
```php
|
|
// 1. Use user-based limits for authenticated users
|
|
$clientId = $this->auth->check()
|
|
? ClientIdentifier::forUser($this->auth->id())
|
|
: ClientIdentifier::forIp($request->server->getClientIp());
|
|
|
|
// 2. Different limits for authenticated vs anonymous
|
|
#[Action(
|
|
rateLimit: 100, // Authenticated users
|
|
rateLimitWindow: 60,
|
|
anonymousLimit: 20 // Anonymous users
|
|
)]
|
|
|
|
// 3. Whitelist trusted IPs
|
|
$trustedIps = ['10.0.0.0/8', '172.16.0.0/12'];
|
|
if ($this->ipWhitelist->isTrusted($clientIp)) {
|
|
// Skip rate limiting
|
|
}
|
|
|
|
// 4. Clear rate limits in tests
|
|
afterEach(function () {
|
|
$cache = container()->get(Cache::class);
|
|
$cache->clear(); // Reset rate limit counters
|
|
});
|
|
```
|
|
|
|
### Idempotency Issues
|
|
|
|
**Problem**: Cached results returned when they shouldn't be
|
|
|
|
**Causes**:
|
|
- TTL too long
|
|
- Key collision
|
|
- Cache not cleared after errors
|
|
- Stale cached errors
|
|
|
|
**Solutions**:
|
|
```php
|
|
// 1. Shorter TTL for volatile data
|
|
#[Action(idempotencyTTL: 300)] // 5 minutes instead of 1 hour
|
|
|
|
// 2. Include more context in key
|
|
$idempotencyKey = sprintf(
|
|
'order-%s-user-%s-%s',
|
|
$orderId,
|
|
$userId,
|
|
$timestamp
|
|
);
|
|
|
|
// 3. Clear cache on errors
|
|
try {
|
|
$result = $this->processOrder($request);
|
|
} catch (\Exception $e) {
|
|
// Don't cache errors
|
|
$this->idempotencyService->forget($idempotencyKey);
|
|
throw $e;
|
|
}
|
|
|
|
// 4. Add version to key for breaking changes
|
|
$idempotencyKey = "v2-order-{$orderId}"; // v1 keys ignored
|
|
```
|
|
|
|
---
|
|
|
|
## Security Checklist
|
|
|
|
Before deploying to production:
|
|
|
|
- [ ] CSRF protection enabled (default: ✅)
|
|
- [ ] Rate limits configured per action
|
|
- [ ] Idempotency TTL appropriate for each action
|
|
- [ ] Security exceptions logged (OWASP events)
|
|
- [ ] HTTPS enforced (CSRF tokens require secure cookies)
|
|
- [ ] Session configuration hardened (Secure, HttpOnly, SameSite)
|
|
- [ ] Security tests passing
|
|
- [ ] Monitoring/alerting for security events
|
|
- [ ] Error messages user-friendly (no internal details)
|
|
- [ ] Rate limit responses include retry-after
|
|
|
|
---
|
|
|
|
## Additional Resources
|
|
|
|
- **Framework Documentation**: `/docs/claude/security-patterns.md`
|
|
- **OWASP Security Events**: `/docs/claude/security-patterns.md#owasp-event-integration`
|
|
- **Testing Guide**: `tests/Feature/LiveComponents/SecurityTest.php`
|
|
- **Example Components**: `src/Application/LiveComponents/Counter/CounterComponent.php`
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
LiveComponents provides **production-ready security** out of the box:
|
|
|
|
✅ **CSRF Protection** - Automatic token injection and validation
|
|
✅ **Rate Limiting** - Configurable per-action limits
|
|
✅ **Idempotency Keys** - Prevents duplicate operations
|
|
✅ **Defense in Depth** - Multiple security layers
|
|
✅ **Framework Integration** - OWASP logging, monitoring
|
|
✅ **Developer-Friendly** - Automatic, minimal configuration
|
|
|
|
All security features are **enabled by default** and require **zero configuration** for basic usage. Advanced scenarios allow fine-grained control via `#[Action]` attributes.
|