103 lines
3.2 KiB
PHP
103 lines
3.2 KiB
PHP
<?php
|
||
|
||
namespace App\Libraries\Radar;
|
||
|
||
/**
|
||
* JWT + access token Google Workspace (Domain-Wide Delegation).
|
||
* Même pattern que application/libraries/GCalService.php (MSOP-4).
|
||
*/
|
||
class GoogleWorkspaceToken
|
||
{
|
||
private string $jsonPath;
|
||
private string $tokenUri = 'https://oauth2.googleapis.com/token';
|
||
|
||
public function __construct(string $jsonPath)
|
||
{
|
||
$this->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'];
|
||
}
|
||
|
||
/**
|
||
* @param list<string> $headers
|
||
* @return array{0:int,1:string}
|
||
*/
|
||
public function http(string $method, string $url, array $headers = [], ?string $body = null): 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];
|
||
}
|
||
|
||
private function b64url(string $data): string
|
||
{
|
||
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
||
}
|
||
}
|