78 lines
2.7 KiB
PHP
78 lines
2.7 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Auth;
|
|
|
|
use App\Models\Tenant;
|
|
use Firebase\JWT\JWK;
|
|
use Firebase\JWT\JWT;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Http;
|
|
use RuntimeException;
|
|
use Throwable;
|
|
|
|
/**
|
|
* Valide les jetons d'accès Entra envoyés par l'add-in (obtenus par NAA).
|
|
* Contrôles : signature (clés publiques Entra), expiration, audience, émetteur,
|
|
* tenant autorisé et actif, scope attendu.
|
|
*/
|
|
class EntraTokenValidator
|
|
{
|
|
/** @return array<string, mixed> claims du jeton */
|
|
public function validate(string $jwt): array
|
|
{
|
|
JWT::$leeway = (int) config('signature.addin.leeway_seconds', 120);
|
|
|
|
try {
|
|
$claims = (array) JWT::decode($jwt, $this->keys());
|
|
} catch (Throwable $e) {
|
|
// Rotation des clés Entra : un seul nouvel essai avec des clés fraîches.
|
|
Cache::forget('entra-jwks');
|
|
try {
|
|
$claims = (array) JWT::decode($jwt, $this->keys());
|
|
} catch (Throwable) {
|
|
throw new RuntimeException('Jeton invalide : '.$e->getMessage());
|
|
}
|
|
}
|
|
|
|
$tid = $claims['tid'] ?? null;
|
|
if (! $tid || ! Tenant::where('entra_tenant_id', $tid)->where('active', true)->exists()) {
|
|
throw new RuntimeException('Tenant non autorisé');
|
|
}
|
|
|
|
$issuers = ["https://login.microsoftonline.com/{$tid}/v2.0", "https://sts.windows.net/{$tid}/"];
|
|
if (! in_array($claims['iss'] ?? null, $issuers, true)) {
|
|
throw new RuntimeException('Émetteur inattendu');
|
|
}
|
|
|
|
$audiences = (array) config('signature.addin.audiences');
|
|
if (! $audiences || ! in_array($claims['aud'] ?? null, $audiences, true)) {
|
|
throw new RuntimeException('Audience inattendue : reçue « '.($claims['aud'] ?? 'aucune').' », attendue(s) « '.implode(', ', $audiences).' »');
|
|
}
|
|
|
|
$scopes = explode(' ', (string) ($claims['scp'] ?? ''));
|
|
if (! in_array(config('signature.addin.required_scope'), $scopes, true)) {
|
|
throw new RuntimeException('Scope manquant : reçu « '.($claims['scp'] ?? 'aucun').' »');
|
|
}
|
|
|
|
if (empty($claims['oid'])) {
|
|
throw new RuntimeException('Identifiant utilisateur absent');
|
|
}
|
|
|
|
return $claims;
|
|
}
|
|
|
|
private function keys(): array
|
|
{
|
|
$jwks = Cache::remember('entra-jwks', config('signature.addin.jwks_cache_seconds', 86400), function () {
|
|
$res = Http::timeout(10)->get('https://login.microsoftonline.com/common/discovery/v2.0/keys');
|
|
if (! $res->successful()) {
|
|
throw new RuntimeException('Clés Entra indisponibles');
|
|
}
|
|
|
|
return $res->json();
|
|
});
|
|
|
|
return JWK::parseKeySet($jwks, 'RS256');
|
|
}
|
|
}
|