conn = $conn; $this->apiKeyModel = new ApiKeyModel($conn); $this->userModel = new UserModel($conn); } /** * Store the validated key's context for later scope/attribution checks. * * @param array $keyData Row returned by ApiKeyModel::validateKey() */ private function setKeyContext(array $keyData) { $this->keyContext = [ 'scope' => $keyData['scope'] ?? 'read_write', 'key_name' => $keyData['key_name'] ?? null, 'created_by' => $keyData['created_by'] ?? null, 'api_key_id' => $keyData['api_key_id'] ?? null, ]; } /** * Get the context of the authenticated API key. * * @return array|null ['scope', 'key_name', 'created_by', 'api_key_id'] or null */ public function getKeyContext(): ?array { return $this->keyContext; } /** * Enforce that the authenticated key satisfies the required scope. * * A 'read' key satisfies only 'read'; a 'read_write' key satisfies both * 'read' and 'read_write'. On failure a 403 JSON error is sent and the * script exits. * * @param string $needed Required scope ('read' or 'read_write') */ public function requireScope(string $needed): void { $current = $this->keyContext['scope'] ?? null; // 'read_write' can do anything; 'read' can only satisfy a 'read' need. $ok = ($current === 'read_write') || ($current === 'read' && $needed === 'read'); if (!$ok) { $this->sendForbidden( 'API key scope "' . ($current ?? 'none') . '" is insufficient; "' . $needed . '" is required' ); exit; } } /** * Authenticate using API key from Authorization header * * @return array User data for system user * @throws Exception if authentication fails */ public function authenticate() { // Get Authorization header $authHeader = $this->getAuthorizationHeader(); if (empty($authHeader)) { $this->sendUnauthorized('Missing Authorization header'); exit; } // Check if it's a Bearer token if (!preg_match('/^Bearer\s+(.+)$/i', $authHeader, $matches)) { $this->sendUnauthorized('Invalid Authorization header format. Expected: Bearer '); exit; } $apiKey = $matches[1]; // Validate API key $keyData = $this->apiKeyModel->validateKey($apiKey); if (!$keyData) { $this->sendUnauthorized('Invalid or expired API key'); exit; } // Record key context (scope / attribution) for callers to inspect. $this->setKeyContext($keyData); // Get system user (or the user who created the key) $user = $this->userModel->getSystemUser(); if (!$user) { $this->sendUnauthorized('System user not found'); exit; } // Add API key info to user data for logging $user['api_key_id'] = $keyData['api_key_id']; $user['api_key_name'] = $keyData['key_name']; return $user; } /** * Get Authorization header from various sources * * @return string|null Authorization header value */ private function getAuthorizationHeader() { // Try different header formats if (isset($_SERVER['HTTP_AUTHORIZATION'])) { return $_SERVER['HTTP_AUTHORIZATION']; } if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) { return $_SERVER['REDIRECT_HTTP_AUTHORIZATION']; } // Check for Authorization in getallheaders if available if (function_exists('getallheaders')) { $headers = getallheaders(); if (isset($headers['Authorization'])) { return $headers['Authorization']; } if (isset($headers['authorization'])) { return $headers['authorization']; } } return null; } /** * Send 401 Unauthorized response * * @param string $message Error message */ private function sendUnauthorized($message) { header('HTTP/1.1 401 Unauthorized'); header('Content-Type: application/json'); echo json_encode([ 'success' => false, 'error' => 'Unauthorized', 'message' => $message ]); } /** * Send 403 Forbidden response (e.g. insufficient scope) * * @param string $message Error message */ private function sendForbidden($message) { header('HTTP/1.1 403 Forbidden'); header('Content-Type: application/json'); echo json_encode([ 'success' => false, 'error' => 'Forbidden', 'message' => $message ]); } /** * Verify API key without throwing errors (for optional auth) * * @return array|null User data or null if not authenticated */ public function verifyOptional() { $authHeader = $this->getAuthorizationHeader(); if (empty($authHeader)) { return null; } if (!preg_match('/^Bearer\s+(.+)$/i', $authHeader, $matches)) { return null; } $apiKey = $matches[1]; $keyData = $this->apiKeyModel->validateKey($apiKey); if (!$keyData) { return null; } // Record key context (scope / attribution) for callers to inspect. $this->setKeyContext($keyData); $user = $this->userModel->getSystemUser(); if ($user) { $user['api_key_id'] = $keyData['api_key_id']; $user['api_key_name'] = $keyData['key_name']; } return $user; } }