jsonPath = $jsonPath; if (! \is_file($this->jsonPath)) { throw new \RuntimeException('Service account JSON introuvable: ' . $this->jsonPath); } } public function getAccessToken(string $impersonateEmail, string $scopes): string { $creds = \json_decode((string) \file_get_contents($this->jsonPath), true); if (! \is_array($creds) || empty($creds['client_email']) || empty($creds['private_key'])) { throw new \RuntimeException('service_account.json invalide'); } $now = \time(); $header = ['alg' => 'RS256', 'typ' => 'JWT']; $claim = [ 'iss' => $creds['client_email'], 'scope' => $scopes, 'aud' => $this->tokenUri, 'exp' => $now + 3600, 'iat' => $now, 'sub' => $impersonateEmail, ]; $unsigned = $this->b64url(\json_encode($header)) . '.' . $this->b64url(\json_encode($claim)); $signature = ''; if (! \openssl_sign($unsigned, $signature, $creds['private_key'], \OPENSSL_ALGO_SHA256)) { throw new \RuntimeException('openssl_sign failed'); } $jwt = $unsigned . '.' . $this->b64url($signature); [$code, $body] = $this->http( 'POST', $this->tokenUri, ['Content-Type: application/x-www-form-urlencoded'], \http_build_query([ 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', 'assertion' => $jwt, ]) ); if ($code !== 200) { throw new \RuntimeException("Token Google error ({$code}): {$body}"); } $tok = \json_decode($body, true); if (empty($tok['access_token'])) { throw new \RuntimeException('Pas d’access_token dans la réponse Google'); } return $tok['access_token']; } /** * HTTP sans dépendre de l’ext curl (souvent absente en cPanel). * * @param list $headers * @return array{0:int,1:string} */ public function http(string $method, string $url, array $headers = [], ?string $body = null): array { if (\function_exists('curl_init')) { return $this->httpCurl($method, $url, $headers, $body); } return $this->httpStream($method, $url, $headers, $body); } /** * @param list $headers * @return array{0:int,1:string} */ private function httpCurl(string $method, string $url, array $headers, ?string $body): array { $ch = \curl_init($url); $opts = [ \CURLOPT_RETURNTRANSFER => true, \CURLOPT_CUSTOMREQUEST => $method, \CURLOPT_TIMEOUT => 60, \CURLOPT_HTTPHEADER => $headers, ]; if ($body !== null) { $opts[\CURLOPT_POSTFIELDS] = $body; } \curl_setopt_array($ch, $opts); $res = \curl_exec($ch); $code = (int) \curl_getinfo($ch, \CURLINFO_HTTP_CODE); if ($res === false) { $err = \curl_error($ch); \curl_close($ch); throw new \RuntimeException('cURL error: ' . $err); } \curl_close($ch); return [$code, $res]; } /** * @param list $headers * @return array{0:int,1:string} */ private function httpStream(string $method, string $url, array $headers, ?string $body): array { $headerLines = $headers; if ($body !== null && $body !== '') { $hasContentType = false; foreach ($headerLines as $h) { if (\stripos($h, 'Content-Type:') === 0) { $hasContentType = true; break; } } if (! $hasContentType) { $headerLines[] = 'Content-Type: application/x-www-form-urlencoded'; } } $opts = [ 'http' => [ 'method' => \strtoupper($method), 'header' => \implode("\r\n", $headerLines), 'timeout' => 60, 'ignore_errors' => true, ], 'ssl' => [ 'verify_peer' => true, 'verify_peer_name' => true, ], ]; if ($body !== null) { $opts['http']['content'] = $body; } $ctx = \stream_context_create($opts); $res = @\file_get_contents($url, false, $ctx); $code = 0; if (isset($http_response_header) && \is_array($http_response_header)) { foreach ($http_response_header as $line) { if (\preg_match('#^HTTP/\S+\s+(\d+)#', $line, $m)) { $code = (int) $m[1]; break; } } } if ($res === false) { throw new \RuntimeException('HTTP stream error for ' . $url); } return [$code, $res]; } private function b64url(string $data): string { return \rtrim(\strtr(\base64_encode($data), '+/', '-_'), '='); } }