# LiveComponents Security Guide **Complete security guide for LiveComponents covering CSRF protection, rate limiting, idempotency, authorization, and best practices.** --- ## Table of Contents 1. [CSRF Protection](#csrf-protection) 2. [Rate Limiting](#rate-limiting) 3. [Idempotency](#idempotency) 4. [Action Allow-List](#action-allow-list) 5. [Authorization](#authorization) 6. [Input Validation](#input-validation) 7. [XSS Prevention](#xss-prevention) 8. [Secure State Management](#secure-state-management) 9. [Security Checklist](#security-checklist) --- ## CSRF Protection ### How It Works LiveComponents automatically protect all actions with CSRF tokens: 1. **Token Generation**: Unique token generated per component instance 2. **Token Validation**: Token validated on every action request 3. **Token Regeneration**: Token regenerated after each action ### Implementation **Server-Side** (Automatic): ```php // CSRF token is automatically generated and validated #[Action] public function increment(): CounterState { // CSRF validation happens automatically before this method is called return $this->state->increment(); } ``` **Client-Side** (Automatic): ```html ``` ### Manual CSRF Token Access If you need to access CSRF tokens manually: ```php use App\Framework\LiveComponents\ComponentRegistry; $registry = container()->get(ComponentRegistry::class); $csrfToken = $registry->generateCsrfToken($componentId); ``` ### CSRF Token Format - **Length**: 32 hexadecimal characters - **Format**: `[a-f0-9]{32}` - **Scope**: Per component instance (not global) ### Best Practices ✅ **DO**: - Let the framework handle CSRF automatically - Include CSRF meta tag in base layout: `` - Use HTTPS in production ❌ **DON'T**: - Disable CSRF protection - Share CSRF tokens between components - Store CSRF tokens in localStorage (use session) --- ## Rate Limiting ### Overview Rate limiting prevents abuse by limiting the number of actions per time period. ### Configuration **Global Rate Limit** (`.env`): ```env LIVECOMPONENT_RATE_LIMIT=60 # 60 requests per minute per component ``` **Per-Action Rate Limit**: ```php #[Action(rateLimit: 10)] // 10 requests per minute for this action public function expensiveOperation(): State { // Implementation } ``` ### Rate Limit Headers When rate limit is exceeded, response includes: ``` HTTP/1.1 429 Too Many Requests X-RateLimit-Limit: 60 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 1640995200 Retry-After: 30 ``` ### Client-Side Handling ```javascript // Automatic retry after Retry-After header LiveComponent.executeAction('counter:demo', 'increment') .catch(error => { if (error.status === 429) { const retryAfter = error.headers['retry-after']; console.log(`Rate limited. Retry after ${retryAfter} seconds`); } }); ``` ### Best Practices ✅ **DO**: - Set appropriate rate limits based on action cost - Use higher limits for read operations, lower for write operations - Monitor rate limit violations ❌ **DON'T**: - Set rate limits too low (hurts UX) - Set rate limits too high (allows abuse) - Ignore rate limit violations --- ## Idempotency ### Overview Idempotency ensures that repeating the same action multiple times has the same effect as performing it once. ### Configuration **Per-Action Idempotency**: ```php #[Action(idempotencyTTL: 60)] // Idempotent for 60 seconds public function processPayment(float $amount): State { // This action will return cached result if called again within 60 seconds return $this->state->processPayment($amount); } ``` ### How It Works 1. **First Request**: Action executes, result cached with idempotency key 2. **Subsequent Requests**: Same idempotency key returns cached result 3. **After TTL**: Cache expires, action can execute again ### Idempotency Key **Client-Side**: ```javascript // Generate unique idempotency key const idempotencyKey = `payment-${Date.now()}-${Math.random()}`; LiveComponent.executeAction('payment:demo', 'processPayment', { amount: 100.00, idempotency_key: idempotencyKey }); ``` **Server-Side** (Automatic): - Idempotency key extracted from request - Key includes: component ID + action name + parameters - Cached result returned if key matches ### Use Cases ✅ **Good for**: - Payment processing - Order creation - Email sending - External API calls ❌ **Not suitable for**: - Incrementing counters - Appending to lists - Time-sensitive operations ### Best Practices ✅ **DO**: - Use idempotency for critical operations - Set appropriate TTL (long enough to prevent duplicates, short enough to allow retries) - Include idempotency key in client requests ❌ **DON'T**: - Use idempotency for operations that should execute multiple times - Set TTL too long (prevents legitimate retries) - Rely solely on idempotency for security --- ## Action Allow-List ### Overview Only methods marked with `#[Action]` can be called from the client. ### Implementation ```php #[LiveComponent('user-profile')] final readonly class UserProfileComponent implements LiveComponentContract { // ✅ Can be called from client #[Action] public function updateProfile(array $data): State { return $this->state->updateProfile($data); } // ❌ Cannot be called from client (no #[Action] attribute) public function internalMethod(): void { // This method is not exposed to the client } // ❌ Reserved methods cannot be actions public function mount(): State { // Reserved method - cannot be called as action } } ``` ### Reserved Methods These methods cannot be actions: - `mount()` - `getRenderData()` - `getId()` - `getState()` - `getData()` - Methods starting with `_` (private convention) ### Security Benefits - **Explicit API**: Only intended methods are callable - **No Accidental Exposure**: Internal methods stay internal - **Clear Intent**: `#[Action]` makes API surface explicit ### Best Practices ✅ **DO**: - Mark all public methods that should be callable with `#[Action]` - Keep internal methods without `#[Action]` - Use descriptive action names ❌ **DON'T**: - Mark internal methods with `#[Action]` - Expose sensitive operations without proper authorization - Use generic action names like `do()` or `execute()` --- ## Authorization ### Overview Use `#[RequiresPermission]` to restrict actions to authorized users. ### Implementation ```php #[LiveComponent('post-editor')] final readonly class PostEditorComponent implements LiveComponentContract { #[Action] #[RequiresPermission('posts.edit')] public function updatePost(array $data): State { return $this->state->updatePost($data); } #[Action] #[RequiresPermission('posts.delete')] public function deletePost(): State { return $this->state->deletePost(); } // Multiple permissions (user needs ALL) #[Action] #[RequiresPermission('posts.edit', 'posts.publish')] public function publishPost(): State { return $this->state->publishPost(); } } ``` ### Permission Checking **Custom Authorization Checker**: ```php use App\Framework\LiveComponents\Security\AuthorizationCheckerInterface; final readonly class CustomAuthorizationChecker implements AuthorizationCheckerInterface { public function hasPermission(string $permission): bool { // Check if current user has permission return $this->user->hasPermission($permission); } public function hasAllPermissions(array $permissions): bool { foreach ($permissions as $permission) { if (!$this->hasPermission($permission)) { return false; } } return true; } } ``` ### Unauthorized Access When user lacks required permission: ```json { "success": false, "error": { "code": "UNAUTHORIZED", "message": "User does not have required permission: posts.edit" } } ``` ### Best Practices ✅ **DO**: - Use authorization for sensitive operations - Check permissions server-side (never trust client) - Use specific permissions (not generic like 'admin') - Log authorization failures ❌ **DON'T**: - Rely on client-side authorization checks - Use overly broad permissions - Skip authorization for write operations - Expose permission names in error messages --- ## Input Validation ### Overview Always validate input in actions before processing. ### Implementation ```php #[Action] public function updateProfile(array $data): State { // Validate input $errors = []; if (empty($data['name'])) { $errors[] = 'Name is required'; } if (isset($data['email']) && !filter_var($data['email'], FILTER_VALIDATE_EMAIL)) { $errors[] = 'Invalid email address'; } if (isset($data['age']) && ($data['age'] < 0 || $data['age'] > 150)) { $errors[] = 'Age must be between 0 and 150'; } if (!empty($errors)) { throw new ValidationException('Validation failed', $errors); } // Process valid data return $this->state->updateProfile($data); } ``` ### Type Safety Use type hints for automatic validation: ```php #[Action] public function addAmount(int $amount): State { // PHP automatically validates $amount is an integer if ($amount < 0) { throw new \InvalidArgumentException('Amount must be positive'); } return $this->state->addAmount($amount); } ``` ### Value Objects Use Value Objects for complex validation: ```php #[Action] public function updateEmail(Email $email): State { // Email Value Object validates format automatically return $this->state->updateEmail($email); } ``` ### Best Practices ✅ **DO**: - Validate all input in actions - Use type hints for automatic validation - Use Value Objects for complex data - Return clear error messages ❌ **DON'T**: - Trust client input - Skip validation for "internal" actions - Expose internal validation details - Use generic error messages --- ## XSS Prevention ### Overview LiveComponents automatically escape output in templates. ### Template Escaping **Automatic Escaping**: ```html