115 lines
4.1 KiB
PHP
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), '+/', '-_'), '=');
|
|
}
|
|
}
|