Files
michaelschiemer/src/Framework/LiveComponents/docs/SECURITY-GUIDE.md
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- 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.
2025-10-25 19:18:37 +02:00

29 KiB

LiveComponents Security Guide

Comprehensive security guide for implementing and using LiveComponents with CSRF protection, rate limiting, and idempotency.

Table of Contents


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:

<!-- 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:

// Automatic - no configuration needed
liveComponent.executeAction('increment');

// The client automatically adds:
// { _csrf_token: 'session_token' }

Manual Token Access

For custom forms or AJAX requests:

// 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

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

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

// Framework defaults (can be overridden)
const DEFAULT_RATE_LIMIT = 60;  // requests
const DEFAULT_WINDOW = 60;       // seconds

Custom Rate Limits per Action

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:

{
    "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

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

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

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:

// 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:

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

// 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:

{
    "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:

{
    "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:

// 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

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

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

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

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:

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

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

// 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:


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
// 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

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

// ✅ 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

// ✅ 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

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

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

// 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:

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:

./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:

// 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:

// 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:

// 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.