Files
crm-ms1/application/libraries/GoogleServiceJWT.php
2026-05-27 11:44:10 -04:00

115 lines
4.1 KiB
PHP

<?php
defined('BASEPATH') or exit('No direct script access allowed');
class GoogleServiceJWT
{
protected $CI;
protected $creds;
protected $cacheFile;
public function __construct()
{
$this->CI =& get_instance();
$cfg = $this->CI->config->item('google');
if (!$cfg || empty($cfg['credentials_path'])) {
throw new Exception('Config google.credentials_path manquante');
}
$json = @file_get_contents($cfg['credentials_path']);
if (!$json) {
throw new Exception('Impossible de lire le service_account.json');
}
$this->creds = json_decode($json, true);
if (empty($this->creds['client_email']) || empty($this->creds['private_key'])) {
throw new Exception('JSON service account invalide (client_email/private_key).');
}
$cacheKey = md5(($cfg['credentials_path'] ?? '').'|'.php_uname(). '|' . phpversion());
$this->cacheFileBase = sys_get_temp_dir().'/google_token_cache_'.$cacheKey.'_';
}
/**
* @param string $scopes Scopes séparés par des espaces (ex: "https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/documents")
* @param ?string $impersonateEmail Email à impersonner (optionnel; utile pour Calendar si Domain-wide Delegation)
*/
public function getAccessToken(string $scopes, ?string $impersonateEmail = null): string
{
$cacheFile = $this->cacheFileBase . md5($scopes.'|'.($impersonateEmail ?: ''));
// Cache simple (~55 min)
if (is_file($cacheFile)) {
$cache = json_decode(@file_get_contents($cacheFile), true);
if (!empty($cache['access_token']) && time() < ($cache['expires_at'] ?? 0)) {
return $cache['access_token'];
}
}
$aud = $this->creds['token_uri'] ?? 'https://oauth2.googleapis.com/token';
$now = time();
$header = ['alg'=>'RS256','typ'=>'JWT'];
$payload = [
'iss' => $this->creds['client_email'],
'scope' => $scopes,
'aud' => $aud,
'exp' => $now + 3600,
'iat' => $now,
];
if ($impersonateEmail) {
$payload['sub'] = $impersonateEmail;
}
$assertion = $this->b64($header).'.'.$this->b64($payload);
$pkey = openssl_pkey_get_private($this->creds['private_key']);
if (!$pkey) {
throw new Exception('Impossible de charger la clé privée du service account.');
}
if (!openssl_sign($assertion, $sig, $pkey, OPENSSL_ALGO_SHA256)) {
throw new Exception('Échec openssl_sign()');
}
$jwt = $assertion.'.'.$this->b64($sig);
// Échange JWT -> access_token
$post = http_build_query([
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion' => $jwt,
]);
$ch = curl_init($aud);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $post,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
]);
$raw = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
if ($raw === false || $code !== 200) {
throw new Exception("Token error ($code): $raw $err");
}
$data = json_decode($raw, true);
if (empty($data['access_token'])) {
throw new Exception('Réponse token invalide: '.$raw);
}
// Cache ~55 min
$ttl = max(300, (int)($data['expires_in'] ?? 3600) - 300);
@file_put_contents($cacheFile, json_encode([
'access_token' => $data['access_token'],
'expires_at' => time() + $ttl,
]));
return $data['access_token'];
}
private function b64($arrOrBin): string
{
$bin = is_string($arrOrBin) ? $arrOrBin : json_encode($arrOrBin);
return rtrim(strtr(base64_encode($bin), '+/', '-_'), '=');
}
}