$permissions Required permission(s) */ public function handle(Request $request, Closure $next, string|array $permissions = []): Response { $token = $request->bearerToken(); if (! $token) { return $this->unauthorised('API token required. Use Authorization: Bearer '); } // Normalise permissions to array if (is_string($permissions)) { $permissions = $permissions ? explode(',', $permissions) : []; } // Get client IP $clientIp = $request->ip(); // Use the first permission for authenticate call, we'll check all below $primaryPermission = $permissions[0] ?? ''; // Authenticate with IP check $result = $this->keyService->authenticate($token, $primaryPermission, $clientIp); if (! $result['success']) { return $this->handleAuthError($result, $clientIp); } /** @var AgentApiKey $key */ $key = $result['key']; // Check all required permissions if multiple specified if (count($permissions) > 1) { foreach (array_slice($permissions, 1) as $permission) { if (! $key->hasPermission($permission)) { return $this->forbidden("Missing required permission: {$permission}", $clientIp); } } } // Store API key in request for downstream use $request->attributes->set('agent_api_key', $key); $request->attributes->set('workspace_id', $key->workspace_id); /** @var Response $response */ $response = $next($request); // Add rate limit headers $rateLimit = $result['rate_limit'] ?? []; if (! empty($rateLimit)) { $response->headers->set('X-RateLimit-Limit', (string) ($rateLimit['limit'] ?? 0)); $response->headers->set('X-RateLimit-Remaining', (string) ($rateLimit['remaining'] ?? 0)); $response->headers->set('X-RateLimit-Reset', (string) ($rateLimit['reset_in_seconds'] ?? 0)); } // Add client IP header for debugging if ($clientIp) { $response->headers->set('X-Client-IP', $clientIp); } return $response; } /** * Handle authentication errors. */ protected function handleAuthError(array $result, ?string $clientIp): Response { $error = $result['error'] ?? 'unknown_error'; $message = $result['message'] ?? 'Authentication failed'; return match ($error) { 'invalid_key' => $this->unauthorised($message, $clientIp), 'key_revoked' => $this->unauthorised($message, $clientIp), 'key_expired' => $this->unauthorised($message, $clientIp), 'ip_not_allowed' => $this->ipForbidden($message, $clientIp), 'permission_denied' => $this->forbidden($message, $clientIp), 'rate_limited' => $this->rateLimited($result, $clientIp), default => $this->unauthorised($message, $clientIp), }; } /** * Return 401 Unauthorised response. */ protected function unauthorised(string $message, ?string $clientIp = null): Response { return response()->json([ 'error' => 'unauthorised', 'message' => $message, ], 401, $this->getBaseHeaders($clientIp)); } /** * Return 403 Forbidden response. */ protected function forbidden(string $message, ?string $clientIp = null): Response { return response()->json([ 'error' => 'forbidden', 'message' => $message, ], 403, $this->getBaseHeaders($clientIp)); } /** * Return 403 Forbidden response for IP restriction. */ protected function ipForbidden(string $message, ?string $clientIp = null): Response { return response()->json([ 'error' => 'ip_not_allowed', 'message' => $message, 'your_ip' => $clientIp, ], 403, $this->getBaseHeaders($clientIp)); } /** * Return 429 Too Many Requests response. */ protected function rateLimited(array $result, ?string $clientIp = null): Response { $rateLimit = $result['rate_limit'] ?? []; $headers = array_merge($this->getBaseHeaders($clientIp), [ 'X-RateLimit-Limit' => (string) ($rateLimit['limit'] ?? 0), 'X-RateLimit-Remaining' => '0', 'X-RateLimit-Reset' => (string) ($rateLimit['reset_in_seconds'] ?? 60), 'Retry-After' => (string) ($rateLimit['reset_in_seconds'] ?? 60), ]); return response()->json([ 'error' => 'rate_limited', 'message' => $result['message'] ?? 'Rate limit exceeded', 'rate_limit' => $rateLimit, ], 429, $headers); } /** * Get base headers to include in all responses. */ protected function getBaseHeaders(?string $clientIp): array { $headers = []; if ($clientIp) { $headers['X-Client-IP'] = $clientIp; } return $headers; } }