* * Convenzioni di ritorno: * - null → SSO non raggiungibile (timeout/connessione) → caller deve fare fallback * - array → risposta JSON parsata (include '_httpStatus' int) * caller deve controllare ['_httpStatus'] e ['error'] per gestire 401/422/ecc. */ class SsoHelper { private string $endpoint; private int $timeoutMs; private string $mode; private string $internalKey; public function __construct() { $this->endpoint = rtrim( self::env('SSO_ENDPOINT', 'http://172.18.0.1:4214'), '/' ); $this->timeoutMs = (int) self::env('SSO_TIMEOUT_MS', '3000'); $this->mode = self::env('SSO_MODE', 'local'); $this->internalKey = self::env('SSO_INTERNAL_KEY', ''); } /** Multi-source env lookup (PHP-FPM Alpine workaround). */ private static function env(string $key, string $default): string { $v = getenv($key); if ($v !== false && $v !== '') return $v; if (!empty($_SERVER[$key])) return (string) $_SERVER[$key]; if (!empty($_ENV[$key])) return (string) $_ENV[$key]; return $default; } public function getMode(): string { return $this->mode; } public function isLocalOnly(): bool { return $this->mode === 'local'; } public function isSsoOnly(): bool { return $this->mode === 'sso_only'; } public function isDual(): bool { return $this->mode === 'dual'; } /** * Login SSO. * @return array|null null = unreachable (fallback locale), array = risposta SSO */ public function login(string $email, string $password, string $product = 'nis2'): ?array { if ($this->isLocalOnly()) return null; return $this->post('/auth/sso/login', [ 'email' => $email, 'password' => $password, 'product' => $product, ]); } /** * Cambio password SSO (richiede JWT Bearer utente). */ public function changePassword(string $jwt, string $currentPassword, string $newPassword): ?array { if ($this->isLocalOnly()) return null; return $this->post('/auth/sso/change-password', [ 'currentPassword' => $currentPassword, 'newPassword' => $newPassword, ], $jwt); } /** * Verifica password senza emettere JWT (uso interno — server-to-server). */ public function verifyPassword(string $email, string $password): ?array { if ($this->isLocalOnly()) return null; return $this->postInternal('/auth/sso/verify-password', [ 'email' => $email, 'password' => $password, ]); } /** * Health check: SSO raggiungibile entro 1s? */ public function isAvailable(): bool { $ch = curl_init($this->endpoint . '/health'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT_MS => 1000, CURLOPT_CONNECTTIMEOUT_MS => 1000, ]); curl_exec($ch); $ok = curl_errno($ch) === 0 && (int) curl_getinfo($ch, CURLINFO_HTTP_CODE) === 200; curl_close($ch); return $ok; } // --- HTTP helpers --- private function post(string $path, array $data, ?string $jwt = null): ?array { $headers = ['Content-Type: application/json']; if ($jwt) { $headers[] = 'Authorization: Bearer ' . preg_replace('/^Bearer\s+/i', '', $jwt); } $ch = curl_init($this->endpoint . $path); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($data), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT_MS => $this->timeoutMs, CURLOPT_CONNECTTIMEOUT_MS => $this->timeoutMs, CURLOPT_HTTPHEADER => $headers, ]); $body = curl_exec($ch); $errno = curl_errno($ch); $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($errno !== 0) return null; $result = json_decode($body, true); if (is_array($result)) { $result['_httpStatus'] = $httpCode; return $result; } return ['_httpStatus' => $httpCode, 'raw' => $body]; } private function postInternal(string $path, array $data): ?array { if ($this->internalKey === '') return null; $ch = curl_init($this->endpoint . $path); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($data), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT_MS => $this->timeoutMs, CURLOPT_CONNECTTIMEOUT_MS => $this->timeoutMs, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'X-Internal-Key: ' . $this->internalKey, ], ]); $body = curl_exec($ch); $errno = curl_errno($ch); curl_close($ch); if ($errno !== 0) return null; return json_decode($body, true); } }